Skip to content

Commit 66e16c9

Browse files
authored
Merge pull request #393 from OpenAPI-Qraft/feat/parameters-wrapper
feat: add `--operation-parameters-type-wrapper` option for custom parameter type wrappers
2 parents ac66e3e + 468485e commit 66e16c9

17 files changed

Lines changed: 3695 additions & 32 deletions

File tree

.changeset/pre.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"mode": "pre",
3+
"tag": "beta",
4+
"initialVersions": {
5+
"@openapi-qraft/e2e": "1.0.0",
6+
"@openapi-qraft/cli": "2.14.0",
7+
"@openapi-qraft/eslint-config": "1.0.1",
8+
"@openapi-qraft/eslint-plugin-query": "2.14.0",
9+
"@openapi-qraft/openapi-typescript-plugin": "2.14.0",
10+
"@openapi-qraft/plugin": "2.14.0",
11+
"@openapi-qraft/react": "2.14.0",
12+
"@openapi-qraft/rollup-config": "1.1.1",
13+
"@openapi-qraft/tanstack-query-react-plugin": "2.14.0",
14+
"@openapi-qraft/tanstack-query-react-types": "2.14.0",
15+
"@openapi-qraft/test-fixtures": "1.1.1",
16+
"@openapi-qraft/ts-factory-code-generator": "1.0.2",
17+
"playground": "0.0.14",
18+
"openapi-qraft-website": "0.0.0"
19+
},
20+
"changesets": []
21+
}

.changeset/short-suits-shine.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openapi-qraft/tanstack-query-react-plugin': minor
3+
---
4+
5+
Add `--operation-parameters-type-wrapper` option to configure custom _ParametersWrapper_ types for specific operation patterns. The ParametersWrapper type now accepts four generic parameters: TSchema, TOperation, TData, and TError, allowing for more flexible parameter type customization.

.github/actions/spelling/expect.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ mycdn
5252
myclient
5353
myqraft
5454
nbf
55+
nnull
5556
nochange
5657
npmrc
5758
npx

packages/cli/README.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,6 @@ The following plugins are currently supported:
209209
- `--openapi-types-import-path ../openapi.d.ts`
210210
- `--openapi-types-import-path '@/api/openapi.d.ts'`
211211
- `--openapi-types-import-path '@external-package-types'`
212-
- **`--operation-generics-import-path <path>`:** Define the path to the operation generics file, allowing for custom
213-
operation handling _(optional, default: `@openapi-qraft/react`)_.
214212
- **`--export-openapi-types [bool]`:** Add an export statement of the generated OpenAPI document types from the `./index.ts` file. Useful for sharing types within your project. _(optional, default: `true`, if `--plugin openapi-typescript` is used)_
215213

216214
### `--plugin openapi-typescript` options
Lines changed: 230 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,238 @@
1+
import type { OpenAPI3 } from 'openapi-typescript';
12
import { describe, expect, it } from 'vitest';
23
import { createServicePathMatch } from './createServicePathMatch.js';
4+
import { filterDocumentPaths } from './filterDocumentPaths.js';
35

4-
describe('createServicePathMatch', () => {
5-
it('should match with multiple include glob', () => {
6-
const isPathMatch = createServicePathMatch(['/user/**', '/post/**']);
7-
expect(isPathMatch('/user/1')).toBe(true);
8-
expect(isPathMatch('/user/1/approve')).toBe(true);
9-
expect(isPathMatch('/post/1')).toBe(true);
10-
expect(isPathMatch('/profile/1')).toBe(false);
11-
});
6+
describe('filterDocumentPaths', () => {
7+
describe('isPathMatch', () => {
8+
it('should match with multiple include glob', () => {
9+
const isPathMatch = createServicePathMatch(['/user/**', '/post/**']);
10+
expect(isPathMatch('/user/1')).toBe(true);
11+
expect(isPathMatch('/user/1/approve')).toBe(true);
12+
expect(isPathMatch('/post/1')).toBe(true);
13+
expect(isPathMatch('/profile/1')).toBe(false);
14+
});
15+
16+
it('should not match with inclusion pattern', () => {
17+
const isPathMatch = createServicePathMatch([
18+
'/user/**',
19+
'/post/**',
20+
'!/user/1',
21+
]);
22+
expect(isPathMatch('/user/1')).toBe(false);
23+
expect(isPathMatch('/user/1/approve')).toBe(true);
24+
expect(isPathMatch('/post/1')).toBe(true);
25+
expect(isPathMatch('/profile/1')).toBe(false);
26+
});
1227

13-
it('should not match with inclusion pattern', () => {
14-
const isPathMatch = createServicePathMatch([
15-
'/user/**',
16-
'/post/**',
17-
'!/user/1',
18-
]);
19-
expect(isPathMatch('/user/1')).toBe(false);
20-
expect(isPathMatch('/user/1/approve')).toBe(true);
21-
expect(isPathMatch('/post/1')).toBe(true);
22-
expect(isPathMatch('/profile/1')).toBe(false);
28+
it('should not match if nothing is specified', () => {
29+
const isPathMatch = createServicePathMatch([]);
30+
expect(isPathMatch('/user/1')).toBe(false);
31+
});
2332
});
2433

25-
it('should not match if nothing is specified', () => {
26-
const isPathMatch = createServicePathMatch([]);
27-
expect(isPathMatch('/user/1')).toBe(false);
34+
describe('filterDocumentPaths', () => {
35+
const createMockSchema = (paths: Record<string, any>): OpenAPI3 => ({
36+
openapi: '3.0.0',
37+
info: {
38+
title: 'Test API',
39+
version: '1.0.0',
40+
},
41+
paths,
42+
components: {},
43+
});
44+
45+
it('should return original schema when servicesGlob is empty', () => {
46+
const schema = createMockSchema({
47+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
48+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
49+
});
50+
51+
const result = filterDocumentPaths(schema, []);
52+
expect(result).toEqual(schema);
53+
});
54+
55+
it('should include all paths matching positive patterns', () => {
56+
const schema = createMockSchema({
57+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
58+
'/user/2': { get: { responses: { 200: { description: 'OK' } } } },
59+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
60+
'/profile/1': { get: { responses: { 200: { description: 'OK' } } } },
61+
});
62+
63+
const result = filterDocumentPaths(schema, ['/user/**', '/post/**']);
64+
expect(result.paths).toEqual({
65+
'/user/1': schema.paths?.['/user/1'],
66+
'/user/2': schema.paths?.['/user/2'],
67+
'/post/1': schema.paths?.['/post/1'],
68+
});
69+
expect(result.paths?.['/profile/1']).toBeUndefined();
70+
});
71+
72+
it('should exclude paths matching negative patterns', () => {
73+
const schema = createMockSchema({
74+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
75+
'/user/2': { get: { responses: { 200: { description: 'OK' } } } },
76+
'/user/internal': {
77+
get: { responses: { 200: { description: 'OK' } } },
78+
},
79+
'/user/internal/secret': {
80+
get: { responses: { 200: { description: 'OK' } } },
81+
},
82+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
83+
});
84+
85+
const result = filterDocumentPaths(schema, [
86+
'/user/**',
87+
'/post/**',
88+
'!/user/internal',
89+
]);
90+
91+
expect(result.paths).toEqual({
92+
'/user/1': schema.paths?.['/user/1'],
93+
'/user/2': schema.paths?.['/user/2'],
94+
'/user/internal/secret': schema.paths?.['/user/internal/secret'],
95+
'/post/1': schema.paths?.['/post/1'],
96+
});
97+
expect(result.paths?.['/user/internal']).toBeUndefined();
98+
});
99+
100+
it('should exclude paths matching negative patterns with single positive pattern', () => {
101+
const schema = createMockSchema({
102+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
103+
'/user/2': { get: { responses: { 200: { description: 'OK' } } } },
104+
'/user/internal': {
105+
get: { responses: { 200: { description: 'OK' } } },
106+
},
107+
'/user/internal/secret': {
108+
get: { responses: { 200: { description: 'OK' } } },
109+
},
110+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
111+
});
112+
113+
const result = filterDocumentPaths(schema, [
114+
'/user/**',
115+
'!/user/internal',
116+
]);
117+
118+
expect(result.paths).toEqual({
119+
'/user/1': schema.paths?.['/user/1'],
120+
'/user/2': schema.paths?.['/user/2'],
121+
'/user/internal/secret': schema.paths?.['/user/internal/secret'],
122+
});
123+
expect(result.paths?.['/user/internal']).toBeUndefined();
124+
expect(result.paths?.['/post/1']).toBeUndefined();
125+
});
126+
127+
it('should handle only negative patterns (excludes everything)', () => {
128+
const schema = createMockSchema({
129+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
130+
'/user/internal': {
131+
get: { responses: { 200: { description: 'OK' } } },
132+
},
133+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
134+
});
135+
136+
const result = filterDocumentPaths(schema, ['!/user/internal']);
137+
138+
// When only negative patterns are provided, micromatch requires at least one positive pattern
139+
// So nothing should match
140+
expect(Object.keys(result.paths as Record<string, unknown>)).toHaveLength(
141+
0
142+
);
143+
});
144+
145+
it('should handle nested paths with negation', () => {
146+
const schema = createMockSchema({
147+
'/user/profile': { get: { responses: { 200: { description: 'OK' } } } },
148+
'/user/profile/settings': {
149+
get: { responses: { 200: { description: 'OK' } } },
150+
},
151+
'/user/internal': {
152+
get: { responses: { 200: { description: 'OK' } } },
153+
},
154+
'/user/internal/admin': {
155+
get: { responses: { 200: { description: 'OK' } } },
156+
},
157+
});
158+
159+
const result = filterDocumentPaths(schema, [
160+
'/user/**',
161+
'!/user/internal/**',
162+
]);
163+
164+
expect(result.paths).toEqual({
165+
'/user/profile': schema.paths?.['/user/profile'],
166+
'/user/profile/settings': schema.paths?.['/user/profile/settings'],
167+
});
168+
expect(result.paths?.['/user/internal']).toBeUndefined();
169+
expect(result.paths?.['/user/internal/admin']).toBeUndefined();
170+
});
171+
172+
it('should handle multiple negative patterns', () => {
173+
const schema = createMockSchema({
174+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
175+
'/user/2': { get: { responses: { 200: { description: 'OK' } } } },
176+
'/user/internal': {
177+
get: { responses: { 200: { description: 'OK' } } },
178+
},
179+
'/user/admin': { get: { responses: { 200: { description: 'OK' } } } },
180+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
181+
});
182+
183+
const result = filterDocumentPaths(schema, [
184+
'/user/**',
185+
'/post/**',
186+
'!/user/internal',
187+
'!/user/admin',
188+
]);
189+
190+
expect(result.paths).toEqual({
191+
'/user/1': schema.paths?.['/user/1'],
192+
'/user/2': schema.paths?.['/user/2'],
193+
'/post/1': schema.paths?.['/post/1'],
194+
});
195+
expect(result.paths?.['/user/internal']).toBeUndefined();
196+
expect(result.paths?.['/user/admin']).toBeUndefined();
197+
});
198+
199+
it('should handle wildcard patterns with negation', () => {
200+
const schema = createMockSchema({
201+
'/api/v1/users': { get: { responses: { 200: { description: 'OK' } } } },
202+
'/api/v1/posts': { get: { responses: { 200: { description: 'OK' } } } },
203+
'/api/v1/internal': {
204+
get: { responses: { 200: { description: 'OK' } } },
205+
},
206+
'/api/v2/users': { get: { responses: { 200: { description: 'OK' } } } },
207+
});
208+
209+
const result = filterDocumentPaths(schema, [
210+
'/api/**',
211+
'!/api/v1/internal',
212+
]);
213+
214+
expect(result.paths).toEqual({
215+
'/api/v1/users': schema.paths?.['/api/v1/users'],
216+
'/api/v1/posts': schema.paths?.['/api/v1/posts'],
217+
'/api/v2/users': schema.paths?.['/api/v2/users'],
218+
});
219+
expect(result.paths?.['/api/v1/internal']).toBeUndefined();
220+
});
221+
222+
it('should preserve schema structure except paths', () => {
223+
const schema = createMockSchema({
224+
'/user/1': { get: { responses: { 200: { description: 'OK' } } } },
225+
'/post/1': { get: { responses: { 200: { description: 'OK' } } } },
226+
});
227+
228+
const result = filterDocumentPaths(schema, ['/user/**']);
229+
230+
expect(result.openapi).toBe(schema.openapi);
231+
expect(result.info).toEqual(schema.info);
232+
expect(result.components).toEqual(schema.components);
233+
expect(Object.keys(result.paths as Record<string, unknown>)).toEqual([
234+
'/user/1',
235+
]);
236+
});
28237
});
29238
});

packages/react-client/redocly.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,10 @@ apis:
7676
createInternalReactAPIClient:
7777
services: none
7878
callbacks: ['setQueryData', 'getQueryData', 'getQueryKey', 'getInfiniteQueryKey']
79+
operation-parameters-type-wrapper:
80+
'get /files/**':
81+
type: 'ParametersWrapper'
82+
import: '../../type-overrides/parameters-wrapper.js'
83+
'delete /approval_policies/{approval_policy_id}':
84+
type: 'ParametersWrapper'
85+
import: '../../type-overrides/parameters-wrapper.js'
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
export type ParametersWrapper<
3+
TSchema extends { method: string; url: string },
4+
TOperation extends { parameters: Record<string, any> },
5+
TData,
6+
TError,
7+
> = TOperation['parameters'];

0 commit comments

Comments
 (0)