@codewithagents/openapi-react-query 3.4.0 → 3.5.1
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 +14 -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 +8 -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
|
@@ -17,7 +17,7 @@ describe('generateHooks — task-hooks.json fixture', () => {
|
|
|
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
|
|
|
@@ -40,8 +40,8 @@ describe('generateHooks — task-hooks.json fixture', () => {
|
|
|
40
40
|
it('generates taskKeys factory with all(), list(), and detail() entries', () => {
|
|
41
41
|
expect(content).toContain('export const taskKeys')
|
|
42
42
|
expect(content).toContain("all: () => ['tasks'] as const")
|
|
43
|
-
expect(content).toContain(
|
|
44
|
-
expect(content).toContain(
|
|
43
|
+
expect(content).toContain('list:')
|
|
44
|
+
expect(content).toContain('detail:')
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
it('useListTasks uses taskKeys.list(params) as queryKey', () => {
|
|
@@ -269,13 +269,19 @@ describe('generateHooks — Bug #53: key factory includes query params when oper
|
|
|
269
269
|
})
|
|
270
270
|
|
|
271
271
|
it('key factory detail entry includes required params when path + required query params', () => {
|
|
272
|
-
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
272
|
+
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
273
|
+
staleTime: 0,
|
|
274
|
+
gcTime: 0,
|
|
275
|
+
})
|
|
273
276
|
// Key factory should accept required params (no ?)
|
|
274
277
|
expect(content).toContain('(id: string, params: Parameters<typeof getReport>[1])')
|
|
275
278
|
})
|
|
276
279
|
|
|
277
280
|
it('hook marks params as required when path param + required query param', () => {
|
|
278
|
-
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
281
|
+
const { content } = generateHooks(specWithPathAndRequiredQueryParams, {
|
|
282
|
+
staleTime: 0,
|
|
283
|
+
gcTime: 0,
|
|
284
|
+
})
|
|
279
285
|
expect(content).toContain('params: Parameters<typeof getReport>[1]')
|
|
280
286
|
expect(content).not.toContain('params?: Parameters<typeof getReport>[1]')
|
|
281
287
|
})
|
|
@@ -284,25 +290,43 @@ describe('generateHooks — Bug #53: key factory includes query params when oper
|
|
|
284
290
|
// ── Bug #3: Mutation hook argument shapes ───────────────────────────────────────
|
|
285
291
|
|
|
286
292
|
describe('generateHooks — Bug #54: mutation hook handles all argument shapes', () => {
|
|
287
|
-
const makeSpec = (
|
|
293
|
+
const makeSpec = (
|
|
294
|
+
pathSegment: string,
|
|
295
|
+
method: string,
|
|
296
|
+
hasBody: boolean,
|
|
297
|
+
queryParams: Array<{ name: string; required?: boolean }>
|
|
298
|
+
): OpenAPIV3_1.Document => ({
|
|
288
299
|
openapi: '3.1.0',
|
|
289
300
|
info: { title: 'Test', version: '1.0.0' },
|
|
290
301
|
paths: {
|
|
291
302
|
[pathSegment]: {
|
|
292
303
|
[method]: {
|
|
293
304
|
operationId: `${method}Op`,
|
|
294
|
-
parameters: queryParams.map((p) => ({
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
305
|
+
parameters: queryParams.map((p) => ({
|
|
306
|
+
name: p.name,
|
|
307
|
+
in: 'query' as const,
|
|
308
|
+
required: p.required,
|
|
309
|
+
schema: { type: 'string' as const },
|
|
310
|
+
})),
|
|
311
|
+
...(hasBody
|
|
312
|
+
? {
|
|
313
|
+
requestBody: {
|
|
314
|
+
required: true,
|
|
315
|
+
content: {
|
|
316
|
+
'application/json': { schema: { $ref: '#/components/schemas/Body' } },
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
: {}),
|
|
301
321
|
responses: { '200': { description: 'ok' } },
|
|
302
322
|
},
|
|
303
323
|
},
|
|
304
324
|
},
|
|
305
|
-
components: {
|
|
325
|
+
components: {
|
|
326
|
+
schemas: {
|
|
327
|
+
Body: { type: 'object' as const, properties: { name: { type: 'string' as const } } },
|
|
328
|
+
},
|
|
329
|
+
},
|
|
306
330
|
})
|
|
307
331
|
|
|
308
332
|
it('mutation with query params only (no body, no path): variables type is Parameters[0], mutationFn is (vars) => fn(vars)', () => {
|
|
@@ -344,7 +368,9 @@ describe('generateHooks — Bug #54: mutation hook handles all argument shapes',
|
|
|
344
368
|
},
|
|
345
369
|
},
|
|
346
370
|
},
|
|
347
|
-
components: {
|
|
371
|
+
components: {
|
|
372
|
+
schemas: { Body: { type: 'object', properties: { name: { type: 'string' } } } },
|
|
373
|
+
},
|
|
348
374
|
}
|
|
349
375
|
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
350
376
|
// Variables includes id, body, and params
|
|
@@ -391,13 +417,17 @@ describe('generateHooks — Bug #54: mutation hook handles all argument shapes',
|
|
|
391
417
|
operationId: 'createTask',
|
|
392
418
|
requestBody: {
|
|
393
419
|
required: true,
|
|
394
|
-
content: {
|
|
420
|
+
content: {
|
|
421
|
+
'application/json': { schema: { $ref: '#/components/schemas/CreateTask' } },
|
|
422
|
+
},
|
|
395
423
|
},
|
|
396
424
|
responses: { '201': { description: 'created' } },
|
|
397
425
|
},
|
|
398
426
|
},
|
|
399
427
|
},
|
|
400
|
-
components: {
|
|
428
|
+
components: {
|
|
429
|
+
schemas: { CreateTask: { type: 'object', properties: { title: { type: 'string' } } } },
|
|
430
|
+
},
|
|
401
431
|
}
|
|
402
432
|
const { content } = generateHooks(spec, { staleTime: 0, gcTime: 0 })
|
|
403
433
|
expect(content).toContain('Parameters<typeof createTask>[0]')
|
|
@@ -414,7 +444,14 @@ describe('generateHooks — tag name to camelCase key factory', () => {
|
|
|
414
444
|
[`/${pathSegment}`]: {
|
|
415
445
|
get: {
|
|
416
446
|
operationId: `list${pathSegment}`,
|
|
417
|
-
responses: {
|
|
447
|
+
responses: {
|
|
448
|
+
'200': {
|
|
449
|
+
description: 'ok',
|
|
450
|
+
content: {
|
|
451
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
},
|
|
418
455
|
},
|
|
419
456
|
},
|
|
420
457
|
},
|
|
@@ -746,7 +783,11 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
746
783
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
747
784
|
requestBody: {
|
|
748
785
|
required: true,
|
|
749
|
-
content: {
|
|
786
|
+
content: {
|
|
787
|
+
'application/json': {
|
|
788
|
+
schema: { type: 'object', properties: { name: { type: 'string' } } },
|
|
789
|
+
},
|
|
790
|
+
},
|
|
750
791
|
},
|
|
751
792
|
responses: { '200': { description: 'ok' } },
|
|
752
793
|
},
|
|
@@ -762,13 +803,21 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
762
803
|
}
|
|
763
804
|
|
|
764
805
|
it('key factory does NOT use "detail:" when there are multiple detail ops', () => {
|
|
765
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
806
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
807
|
+
staleTime: 0,
|
|
808
|
+
gcTime: 0,
|
|
809
|
+
autoInvalidate: true,
|
|
810
|
+
})
|
|
766
811
|
// With 2 detail ops (getItem + getItemSummary), the key factory uses operation-derived names
|
|
767
812
|
expect(content).not.toContain('detail:')
|
|
768
813
|
})
|
|
769
814
|
|
|
770
815
|
it('mutation hook does NOT reference itemKeys.detail( when there are multiple detail ops', () => {
|
|
771
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
816
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
817
|
+
staleTime: 0,
|
|
818
|
+
gcTime: 0,
|
|
819
|
+
autoInvalidate: true,
|
|
820
|
+
})
|
|
772
821
|
const updateStart = content.indexOf('export function useUpdateItem')
|
|
773
822
|
const updateEnd = content.indexOf('\n}', updateStart) + 2
|
|
774
823
|
const updateContent = content.slice(updateStart, updateEnd)
|
|
@@ -776,7 +825,11 @@ describe('generateHooks — Bug #75: auto-invalidation skips detail when multipl
|
|
|
776
825
|
})
|
|
777
826
|
|
|
778
827
|
it('mutation hook DOES contain itemKeys.all() for broad invalidation', () => {
|
|
779
|
-
const { content } = generateHooks(multiDetailSpec, {
|
|
828
|
+
const { content } = generateHooks(multiDetailSpec, {
|
|
829
|
+
staleTime: 0,
|
|
830
|
+
gcTime: 0,
|
|
831
|
+
autoInvalidate: true,
|
|
832
|
+
})
|
|
780
833
|
const updateStart = content.indexOf('export function useUpdateItem')
|
|
781
834
|
const updateEnd = content.indexOf('\n}', updateStart) + 2
|
|
782
835
|
const updateContent = content.slice(updateStart, updateEnd)
|
|
@@ -892,7 +945,11 @@ describe('feature interactions', () => {
|
|
|
892
945
|
|
|
893
946
|
describe('auto-invalidate mutations (#58)', () => {
|
|
894
947
|
it('autoInvalidate: false — no useQueryClient or invalidateQueries in output', () => {
|
|
895
|
-
const { content } = generateHooks(spec, {
|
|
948
|
+
const { content } = generateHooks(spec, {
|
|
949
|
+
staleTime: 0,
|
|
950
|
+
gcTime: 300_000,
|
|
951
|
+
autoInvalidate: false,
|
|
952
|
+
})
|
|
896
953
|
expect(content).not.toContain('useQueryClient')
|
|
897
954
|
expect(content).not.toContain('invalidateQueries')
|
|
898
955
|
})
|
|
@@ -975,7 +1032,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
975
1032
|
operationId: 'postC',
|
|
976
1033
|
requestBody: {
|
|
977
1034
|
required: true,
|
|
978
|
-
content: {
|
|
1035
|
+
content: {
|
|
1036
|
+
'application/json': {
|
|
1037
|
+
schema: {
|
|
1038
|
+
type: 'object' as const,
|
|
1039
|
+
properties: { name: { type: 'string' as const } },
|
|
1040
|
+
},
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
979
1043
|
},
|
|
980
1044
|
responses: { '201': { description: 'created' } },
|
|
981
1045
|
},
|
|
@@ -987,7 +1051,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
987
1051
|
parameters: [{ name: 'dryRun', in: 'query', schema: { type: 'string' } }],
|
|
988
1052
|
requestBody: {
|
|
989
1053
|
required: true,
|
|
990
|
-
content: {
|
|
1054
|
+
content: {
|
|
1055
|
+
'application/json': {
|
|
1056
|
+
schema: {
|
|
1057
|
+
type: 'object' as const,
|
|
1058
|
+
properties: { name: { type: 'string' as const } },
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
991
1062
|
},
|
|
992
1063
|
responses: { '200': { description: 'ok' } },
|
|
993
1064
|
},
|
|
@@ -1018,7 +1089,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1018
1089
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
1019
1090
|
requestBody: {
|
|
1020
1091
|
required: true,
|
|
1021
|
-
content: {
|
|
1092
|
+
content: {
|
|
1093
|
+
'application/json': {
|
|
1094
|
+
schema: {
|
|
1095
|
+
type: 'object' as const,
|
|
1096
|
+
properties: { name: { type: 'string' as const } },
|
|
1097
|
+
},
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1022
1100
|
},
|
|
1023
1101
|
responses: { '200': { description: 'ok' } },
|
|
1024
1102
|
},
|
|
@@ -1033,7 +1111,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1033
1111
|
],
|
|
1034
1112
|
requestBody: {
|
|
1035
1113
|
required: true,
|
|
1036
|
-
content: {
|
|
1114
|
+
content: {
|
|
1115
|
+
'application/json': {
|
|
1116
|
+
schema: {
|
|
1117
|
+
type: 'object' as const,
|
|
1118
|
+
properties: { name: { type: 'string' as const } },
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1037
1122
|
},
|
|
1038
1123
|
responses: { '200': { description: 'ok' } },
|
|
1039
1124
|
},
|
|
@@ -1071,7 +1156,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1071
1156
|
],
|
|
1072
1157
|
requestBody: {
|
|
1073
1158
|
required: true,
|
|
1074
|
-
content: {
|
|
1159
|
+
content: {
|
|
1160
|
+
'application/json': {
|
|
1161
|
+
schema: {
|
|
1162
|
+
type: 'object' as const,
|
|
1163
|
+
properties: { name: { type: 'string' as const } },
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1075
1167
|
},
|
|
1076
1168
|
responses: { '200': { description: 'ok' } },
|
|
1077
1169
|
},
|
|
@@ -1087,7 +1179,14 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1087
1179
|
],
|
|
1088
1180
|
requestBody: {
|
|
1089
1181
|
required: true,
|
|
1090
|
-
content: {
|
|
1182
|
+
content: {
|
|
1183
|
+
'application/json': {
|
|
1184
|
+
schema: {
|
|
1185
|
+
type: 'object' as const,
|
|
1186
|
+
properties: { name: { type: 'string' as const } },
|
|
1187
|
+
},
|
|
1188
|
+
},
|
|
1189
|
+
},
|
|
1091
1190
|
},
|
|
1092
1191
|
responses: { '200': { description: 'ok' } },
|
|
1093
1192
|
},
|
|
@@ -1099,28 +1198,38 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1099
1198
|
|
|
1100
1199
|
it('case 1 (0 params, no body, no query): variablesType is void, mutationFn is () => fn()', () => {
|
|
1101
1200
|
// UseMutationOptions<..., ApiError, void> — void is the variables type
|
|
1102
|
-
expect(shapesContent).toContain(
|
|
1201
|
+
expect(shapesContent).toContain(
|
|
1202
|
+
'UseMutationOptions<Awaited<ReturnType<typeof deleteA>>, ApiError, void>'
|
|
1203
|
+
)
|
|
1103
1204
|
expect(shapesContent).toContain('mutationFn: () => deleteA()')
|
|
1104
1205
|
})
|
|
1105
1206
|
|
|
1106
1207
|
it('case 2 (0 params, no body, has query): variablesType is Parameters<typeof fn>[0], mutationFn is (vars) => fn(vars)', () => {
|
|
1107
|
-
expect(shapesContent).toContain(
|
|
1208
|
+
expect(shapesContent).toContain(
|
|
1209
|
+
'UseMutationOptions<Awaited<ReturnType<typeof postB>>, ApiError, Parameters<typeof postB>[0]>'
|
|
1210
|
+
)
|
|
1108
1211
|
expect(shapesContent).toContain('mutationFn: (vars) => postB(vars)')
|
|
1109
1212
|
})
|
|
1110
1213
|
|
|
1111
1214
|
it('case 3 (0 params, has body, no query): variablesType is Parameters<typeof fn>[0], mutationFn is (vars) => fn(vars)', () => {
|
|
1112
|
-
expect(shapesContent).toContain(
|
|
1215
|
+
expect(shapesContent).toContain(
|
|
1216
|
+
'UseMutationOptions<Awaited<ReturnType<typeof postC>>, ApiError, Parameters<typeof postC>[0]>'
|
|
1217
|
+
)
|
|
1113
1218
|
expect(shapesContent).toContain('mutationFn: (vars) => postC(vars)')
|
|
1114
1219
|
})
|
|
1115
1220
|
|
|
1116
1221
|
it('case 4 (0 params, has body, has query): variablesType is { body: ...[0]; params: ...[1] }, mutationFn destructures both', () => {
|
|
1117
|
-
expect(shapesContent).toContain(
|
|
1222
|
+
expect(shapesContent).toContain(
|
|
1223
|
+
'body: Parameters<typeof postD>[0]; params: Parameters<typeof postD>[1]'
|
|
1224
|
+
)
|
|
1118
1225
|
expect(shapesContent).toContain('mutationFn: ({ body, params }) => postD(body, params)')
|
|
1119
1226
|
})
|
|
1120
1227
|
|
|
1121
1228
|
it('case 5 (1 path param, no body, no query): variablesType is string, mutationFn is (id) => fn(id)', () => {
|
|
1122
1229
|
// string is the entire variables type — only case that uses literal 'string'
|
|
1123
|
-
expect(shapesContent).toContain(
|
|
1230
|
+
expect(shapesContent).toContain(
|
|
1231
|
+
'UseMutationOptions<Awaited<ReturnType<typeof deleteE>>, ApiError, string>'
|
|
1232
|
+
)
|
|
1124
1233
|
expect(shapesContent).toContain('mutationFn: (id) => deleteE(id)')
|
|
1125
1234
|
})
|
|
1126
1235
|
|
|
@@ -1135,28 +1244,44 @@ describe('mutation hook variables type — all 12 shapes', () => {
|
|
|
1135
1244
|
})
|
|
1136
1245
|
|
|
1137
1246
|
it('case 8 (1 path param, has body, has query): variablesType is { id: string; body: ...[1]; params: ...[2] }, mutationFn destructures all', () => {
|
|
1138
|
-
expect(shapesContent).toContain(
|
|
1247
|
+
expect(shapesContent).toContain(
|
|
1248
|
+
'id: string; body: Parameters<typeof putH>[1]; params: Parameters<typeof putH>[2]'
|
|
1249
|
+
)
|
|
1139
1250
|
expect(shapesContent).toContain('mutationFn: ({ id, body, params }) => putH(id, body, params)')
|
|
1140
1251
|
})
|
|
1141
1252
|
|
|
1142
1253
|
it('case 9 (2+ params, no body, no query): variablesType is { projectId: string; taskId: string }, mutationFn destructures both path params', () => {
|
|
1143
1254
|
expect(shapesContent).toContain('projectId: string; taskId: string }')
|
|
1144
|
-
expect(shapesContent).toContain(
|
|
1255
|
+
expect(shapesContent).toContain(
|
|
1256
|
+
'mutationFn: ({ projectId, taskId }) => deleteI(projectId, taskId)'
|
|
1257
|
+
)
|
|
1145
1258
|
})
|
|
1146
1259
|
|
|
1147
1260
|
it('case 10 (2+ params, no body, has query): variablesType includes both path params and params, mutationFn passes all', () => {
|
|
1148
|
-
expect(shapesContent).toContain(
|
|
1149
|
-
|
|
1261
|
+
expect(shapesContent).toContain(
|
|
1262
|
+
'projectId: string; taskId: string; params: Parameters<typeof deleteJ>[2]'
|
|
1263
|
+
)
|
|
1264
|
+
expect(shapesContent).toContain(
|
|
1265
|
+
'mutationFn: ({ projectId, taskId, params }) => deleteJ(projectId, taskId, params)'
|
|
1266
|
+
)
|
|
1150
1267
|
})
|
|
1151
1268
|
|
|
1152
1269
|
it('case 11 (2+ params, has body, no query): variablesType includes both path params and body at index [2]', () => {
|
|
1153
|
-
expect(shapesContent).toContain(
|
|
1154
|
-
|
|
1270
|
+
expect(shapesContent).toContain(
|
|
1271
|
+
'projectId: string; taskId: string; body: Parameters<typeof putK>[2]'
|
|
1272
|
+
)
|
|
1273
|
+
expect(shapesContent).toContain(
|
|
1274
|
+
'mutationFn: ({ projectId, taskId, body }) => putK(projectId, taskId, body)'
|
|
1275
|
+
)
|
|
1155
1276
|
})
|
|
1156
1277
|
|
|
1157
1278
|
it('case 12 (2+ params, has body, has query): variablesType includes both path params, body at [2], params at [3]', () => {
|
|
1158
|
-
expect(shapesContent).toContain(
|
|
1159
|
-
|
|
1279
|
+
expect(shapesContent).toContain(
|
|
1280
|
+
'projectId: string; taskId: string; body: Parameters<typeof putL>[2]; params: Parameters<typeof putL>[3]'
|
|
1281
|
+
)
|
|
1282
|
+
expect(shapesContent).toContain(
|
|
1283
|
+
'mutationFn: ({ projectId, taskId, body, params }) => putL(projectId, taskId, body, params)'
|
|
1284
|
+
)
|
|
1160
1285
|
})
|
|
1161
1286
|
})
|
|
1162
1287
|
|
|
@@ -1187,7 +1312,14 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1187
1312
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
1188
1313
|
requestBody: {
|
|
1189
1314
|
required: true,
|
|
1190
|
-
content: {
|
|
1315
|
+
content: {
|
|
1316
|
+
'application/json': {
|
|
1317
|
+
schema: {
|
|
1318
|
+
type: 'object' as const,
|
|
1319
|
+
properties: { name: { type: 'string' as const } },
|
|
1320
|
+
},
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1191
1323
|
},
|
|
1192
1324
|
responses: { '200': { description: 'ok' } },
|
|
1193
1325
|
},
|
|
@@ -1197,7 +1329,14 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1197
1329
|
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'string' } }],
|
|
1198
1330
|
requestBody: {
|
|
1199
1331
|
required: true,
|
|
1200
|
-
content: {
|
|
1332
|
+
content: {
|
|
1333
|
+
'application/json': {
|
|
1334
|
+
schema: {
|
|
1335
|
+
type: 'object' as const,
|
|
1336
|
+
properties: { name: { type: 'string' as const } },
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1201
1340
|
},
|
|
1202
1341
|
responses: { '200': { description: 'ok' } },
|
|
1203
1342
|
},
|
|
@@ -1211,7 +1350,11 @@ describe('auto-invalidate: method-based detail invalidation', () => {
|
|
|
1211
1350
|
},
|
|
1212
1351
|
}
|
|
1213
1352
|
|
|
1214
|
-
const { content: thingsContent } = generateHooks(thingsSpec, {
|
|
1353
|
+
const { content: thingsContent } = generateHooks(thingsSpec, {
|
|
1354
|
+
staleTime: 0,
|
|
1355
|
+
gcTime: 0,
|
|
1356
|
+
autoInvalidate: true,
|
|
1357
|
+
})
|
|
1215
1358
|
|
|
1216
1359
|
it('thingKeys key factory has a detail entry (exactly one detail GET op)', () => {
|
|
1217
1360
|
expect(thingsContent).toContain('export const thingKeys')
|
|
@@ -1285,7 +1428,10 @@ describe('query hook key builder — 2+ path params with query params', () => {
|
|
|
1285
1428
|
},
|
|
1286
1429
|
}
|
|
1287
1430
|
|
|
1288
|
-
const { content: multiParamContent } = generateHooks(multiParamQuerySpec, {
|
|
1431
|
+
const { content: multiParamContent } = generateHooks(multiParamQuerySpec, {
|
|
1432
|
+
staleTime: 0,
|
|
1433
|
+
gcTime: 0,
|
|
1434
|
+
})
|
|
1289
1435
|
|
|
1290
1436
|
it('key factory detail entry accepts both path params as typed args', () => {
|
|
1291
1437
|
expect(multiParamContent).toContain('(projectId: string, taskId: string,')
|
|
@@ -1297,7 +1443,9 @@ describe('query hook key builder — 2+ path params with query params', () => {
|
|
|
1297
1443
|
})
|
|
1298
1444
|
|
|
1299
1445
|
it('enabled guard ANDs both path params', () => {
|
|
1300
|
-
expect(multiParamContent).toContain(
|
|
1446
|
+
expect(multiParamContent).toContain(
|
|
1447
|
+
'enabled: projectId != null && taskId != null && (options?.enabled ?? true)'
|
|
1448
|
+
)
|
|
1301
1449
|
})
|
|
1302
1450
|
|
|
1303
1451
|
it('queryKey call includes both path params with non-null assertions and query params', () => {
|
|
@@ -1317,7 +1465,10 @@ describe('generateHooks — edge cases', () => {
|
|
|
1317
1465
|
post: {
|
|
1318
1466
|
operationId: 'createPet',
|
|
1319
1467
|
deprecated: true,
|
|
1320
|
-
requestBody: {
|
|
1468
|
+
requestBody: {
|
|
1469
|
+
required: true,
|
|
1470
|
+
content: { 'application/json': { schema: { type: 'object' } } },
|
|
1471
|
+
},
|
|
1321
1472
|
responses: { '201': { description: 'Created' } },
|
|
1322
1473
|
},
|
|
1323
1474
|
},
|
|
@@ -1367,14 +1518,28 @@ describe('generateHooks — edge cases', () => {
|
|
|
1367
1518
|
'/items': {
|
|
1368
1519
|
get: {
|
|
1369
1520
|
operationId: 'listItems',
|
|
1370
|
-
responses: {
|
|
1521
|
+
responses: {
|
|
1522
|
+
'200': {
|
|
1523
|
+
description: 'ok',
|
|
1524
|
+
content: {
|
|
1525
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
1526
|
+
},
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1371
1529
|
},
|
|
1372
1530
|
},
|
|
1373
1531
|
'/items/search': {
|
|
1374
1532
|
get: {
|
|
1375
1533
|
operationId: 'searchItems',
|
|
1376
1534
|
parameters: [{ name: 'q', in: 'query', schema: { type: 'string' } }],
|
|
1377
|
-
responses: {
|
|
1535
|
+
responses: {
|
|
1536
|
+
'200': {
|
|
1537
|
+
description: 'ok',
|
|
1538
|
+
content: {
|
|
1539
|
+
'application/json': { schema: { type: 'array', items: { type: 'object' } } },
|
|
1540
|
+
},
|
|
1541
|
+
},
|
|
1542
|
+
},
|
|
1378
1543
|
},
|
|
1379
1544
|
},
|
|
1380
1545
|
},
|
|
@@ -1413,7 +1578,10 @@ describe('Bug #105: cache key collision when multiple GET ops share same path pa
|
|
|
1413
1578
|
},
|
|
1414
1579
|
}
|
|
1415
1580
|
|
|
1416
|
-
const { content: multiDetailResult } = generateHooks(specWithMultipleDetailOps, {
|
|
1581
|
+
const { content: multiDetailResult } = generateHooks(specWithMultipleDetailOps, {
|
|
1582
|
+
staleTime: 0,
|
|
1583
|
+
gcTime: 0,
|
|
1584
|
+
})
|
|
1417
1585
|
|
|
1418
1586
|
it('both operations have distinct named entries in the key factory', () => {
|
|
1419
1587
|
expect(multiDetailResult).toContain('getItemById:')
|
|
@@ -1504,7 +1672,9 @@ describe('generateHooks — path segment sanitization in deriveOperationName', (
|
|
|
1504
1672
|
paths: {
|
|
1505
1673
|
'/api-keys/{api-key-id}': {
|
|
1506
1674
|
get: {
|
|
1507
|
-
parameters: [
|
|
1675
|
+
parameters: [
|
|
1676
|
+
{ name: 'api-key-id', in: 'path', required: true, schema: { type: 'string' } },
|
|
1677
|
+
],
|
|
1508
1678
|
responses: { '200': { description: 'ok' } },
|
|
1509
1679
|
},
|
|
1510
1680
|
},
|
|
@@ -1547,7 +1717,14 @@ describe('generateHooks — toKeyFactoryName sanitizes special chars', () => {
|
|
|
1547
1717
|
'/attachments/{attachment-publicUUID}': {
|
|
1548
1718
|
get: {
|
|
1549
1719
|
operationId: 'getAttachment',
|
|
1550
|
-
parameters: [
|
|
1720
|
+
parameters: [
|
|
1721
|
+
{
|
|
1722
|
+
name: 'attachment-publicUUID',
|
|
1723
|
+
in: 'path',
|
|
1724
|
+
required: true,
|
|
1725
|
+
schema: { type: 'string' },
|
|
1726
|
+
},
|
|
1727
|
+
],
|
|
1551
1728
|
responses: { '200': { description: 'ok' } },
|
|
1552
1729
|
},
|
|
1553
1730
|
},
|
|
@@ -1576,7 +1753,10 @@ describe('Bug #106: ...options spread must not overwrite auto-invalidation onSuc
|
|
|
1576
1753
|
},
|
|
1577
1754
|
post: {
|
|
1578
1755
|
operationId: 'createItem',
|
|
1579
|
-
requestBody: {
|
|
1756
|
+
requestBody: {
|
|
1757
|
+
required: true,
|
|
1758
|
+
content: { 'application/json': { schema: { $ref: '#/components/schemas/Item' } } },
|
|
1759
|
+
},
|
|
1580
1760
|
responses: { '201': { description: 'ok' } },
|
|
1581
1761
|
},
|
|
1582
1762
|
},
|
|
@@ -1615,7 +1795,11 @@ describe('Bug #106: ...options spread must not overwrite auto-invalidation onSuc
|
|
|
1615
1795
|
})
|
|
1616
1796
|
|
|
1617
1797
|
it('mutation hook without auto-invalidation is unaffected (no onSuccess generated)', () => {
|
|
1618
|
-
const { content: plain } = generateHooks(specWithAutoInvalidate, {
|
|
1798
|
+
const { content: plain } = generateHooks(specWithAutoInvalidate, {
|
|
1799
|
+
staleTime: 0,
|
|
1800
|
+
gcTime: 0,
|
|
1801
|
+
autoInvalidate: false,
|
|
1802
|
+
})
|
|
1619
1803
|
const hookStart = plain.indexOf('export function useCreateItem')
|
|
1620
1804
|
const hookEnd = plain.indexOf('\n}', hookStart) + 2
|
|
1621
1805
|
const hookContent = plain.slice(hookStart, hookEnd)
|
|
@@ -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',
|
|
23
|
-
'/
|
|
24
|
-
'
|
|
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',
|
|
29
|
-
'/
|
|
30
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
}
|