@codewithagents/openapi-react-query 3.3.1 → 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.
@@ -24,7 +24,7 @@ async function runGenerator(): Promise<string> {
24
24
  await writeFile(
25
25
  configPath,
26
26
  JSON.stringify({ input_openapi: taskHooksFixture, output: outDir }),
27
- 'utf-8',
27
+ 'utf-8'
28
28
  )
29
29
  await generate(tmpDir, configPath)
30
30
  return outDir
@@ -17,7 +17,7 @@ describe('generateTestUtils', () => {
17
17
 
18
18
  it('contains auto-generated header comment', () => {
19
19
  expect(content).toContain(
20
- '// This file is auto-generated by @codewithagents/openapi-react-query — do not edit',
20
+ '// This file is auto-generated by @codewithagents/openapi-react-query — do not edit'
21
21
  )
22
22
  })
23
23
 
package/src/config.ts CHANGED
@@ -19,15 +19,33 @@ export interface ReactQueryConfig {
19
19
  }
20
20
 
21
21
  const FORBIDDEN_OUTPUT_PREFIXES = [
22
- '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64',
23
- '/sys', '/proc', '/dev', '/boot', '/run',
24
- 'C:\\Windows', 'C:\\Program Files',
22
+ '/etc',
23
+ '/usr',
24
+ '/bin',
25
+ '/sbin',
26
+ '/lib',
27
+ '/lib64',
28
+ '/sys',
29
+ '/proc',
30
+ '/dev',
31
+ '/boot',
32
+ '/run',
33
+ 'C:\\Windows',
34
+ 'C:\\Program Files',
25
35
  ]
26
36
 
27
37
  const FORBIDDEN_INPUT_PREFIXES = [
28
- '/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64',
29
- '/sys', '/proc', '/dev',
30
- 'C:\\Windows', 'C:\\Program Files',
38
+ '/etc',
39
+ '/usr',
40
+ '/bin',
41
+ '/sbin',
42
+ '/lib',
43
+ '/lib64',
44
+ '/sys',
45
+ '/proc',
46
+ '/dev',
47
+ 'C:\\Windows',
48
+ 'C:\\Program Files',
31
49
  ]
32
50
 
33
51
  export function validateConfigPath(configPath: string): void {
@@ -44,7 +62,7 @@ export function validateOutputPath(resolvedOutput: string): void {
44
62
  if (normalized === normalizedForbidden || normalized.startsWith(normalizedForbidden + '/')) {
45
63
  throw new Error(
46
64
  `Output path resolves to a system directory: "${resolvedOutput}". ` +
47
- `This looks like a misconfiguration — please check your config file.`
65
+ `This looks like a misconfiguration — please check your config file.`
48
66
  )
49
67
  }
50
68
  }
@@ -57,7 +75,7 @@ export function validateInputPath(resolvedInput: string): void {
57
75
  if (normalized === normalizedForbidden || normalized.startsWith(normalizedForbidden + '/')) {
58
76
  throw new Error(
59
77
  `Input spec path resolves to a system directory: "${resolvedInput}". ` +
60
- `This looks like a misconfiguration — please check your config file.`
78
+ `This looks like a misconfiguration — please check your config file.`
61
79
  )
62
80
  }
63
81
  }
@@ -110,10 +128,16 @@ export async function loadConfig(cwd: string, configPath?: string): Promise<Reac
110
128
  throw new Error('"auto_invalidate" must be a boolean')
111
129
  }
112
130
  if (config['overrides'] !== undefined) {
113
- if (typeof config['overrides'] !== 'object' || config['overrides'] === null || Array.isArray(config['overrides'])) {
131
+ if (
132
+ typeof config['overrides'] !== 'object' ||
133
+ config['overrides'] === null ||
134
+ Array.isArray(config['overrides'])
135
+ ) {
114
136
  throw new Error('"overrides" must be an object')
115
137
  }
116
- for (const [resource, timing] of Object.entries(config['overrides'] as Record<string, unknown>)) {
138
+ for (const [resource, timing] of Object.entries(
139
+ config['overrides'] as Record<string, unknown>
140
+ )) {
117
141
  if (typeof timing !== 'object' || timing === null) {
118
142
  throw new Error(`"overrides.${resource}" must be an object`)
119
143
  }
@@ -139,7 +163,9 @@ export async function loadConfig(cwd: string, configPath?: string): Promise<Reac
139
163
  stale_time: config['stale_time'] as number | undefined,
140
164
  gc_time: config['gc_time'] as number | undefined,
141
165
  suspense: config['suspense'] as boolean | undefined,
142
- overrides: config['overrides'] as Record<string, { stale_time?: number; gc_time?: number }> | undefined,
166
+ overrides: config['overrides'] as
167
+ | Record<string, { stale_time?: number; gc_time?: number }>
168
+ | undefined,
143
169
  auto_invalidate: config['auto_invalidate'] as boolean | undefined,
144
170
  }
145
171
  }
@@ -102,13 +102,21 @@ interface OperationMeta {
102
102
  deprecated: boolean
103
103
  }
104
104
 
105
- function getBodyInfo(operation: OperationObject): { hasBody: boolean; bodyTypeName: string | undefined } {
106
- const requestBody = operation.requestBody as OpenAPIV3_1.RequestBodyObject | ReferenceObject | undefined
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 Record<string, { schema?: OpenAPIV3_1.SchemaObject | ReferenceObject }> | undefined
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 // e.g. 'list' | 'detail' | operationId-derived
141
- funcName: string // function to use in key factory
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 = entry.pathParams.length > 0 && entry.key !== 'detail' ? `'${entry.key}', ` : ''
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(` ${entry.key}: (${paramsArg}) => ['${resource}', '${entry.key}', params] as const,`)
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(` ${entry.key}: (${param}: string) => ['${resource}', ${keySegment}${param}] as const,`)
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(` ${entry.key}: (${param}: string, ${paramsArg}) => ['${resource}', ${keySegment}${param}, params] as const,`)
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(` ${entry.key}: (${paramList}) => ['${resource}', ${keySegment}${paramValues}] as const,`)
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(` ${entry.key}: (${paramList}, ${paramsArg}) => ['${resource}', ${keySegment}${paramValues}, params] as const,`)
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(`options?: Omit<UseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`)
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(`options?: Omit<UseSuspenseQueryOptions<Awaited<ReturnType<typeof ${op.funcName}>>, ApiError>, 'queryKey' | 'queryFn'>`)
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(` options?: Omit<UseMutationOptions<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>, 'mutationFn'>,`)
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(` return useMutation<Awaited<ReturnType<typeof ${funcName}>>, ApiError, ${variablesType}>({`)
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(` queryClient.invalidateQueries({ queryKey: ${keyFactoryName}.${detailKeyName}(${detailIdRef}) })`)
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 (OpenAPIV3_1.ParameterObject | ReferenceObject)[] | undefined
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(['break','case','catch','class','const','continue','debugger','default',
513
- 'delete','do','else','export','extends','finally','for','function','if','import','in',
514
- 'instanceof','let','new','return','static','super','switch','this','throw','try','typeof',
515
- 'var','void','while','with','yield'])
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 (OpenAPIV3_1.ParameterObject | ReferenceObject)[] | undefined
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(buildQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime))
733
+ queryHookBlocks.push(
734
+ buildQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
735
+ )
668
736
  if (suspense) {
669
- queryHookBlocks.push(buildSuspenseQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime))
737
+ queryHookBlocks.push(
738
+ buildSuspenseQueryHook(op, factoryName, keyEntry, effectiveStaleTime, effectiveGcTime)
739
+ )
670
740
  }
671
741
  }
672
742
 
@@ -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,