Skip to content

Commit d99e92d

Browse files
committed
feat: enhance oapiRef function with options for deep extraction and property filtering
1 parent a57740d commit d99e92d

8 files changed

Lines changed: 742 additions & 2 deletions

File tree

packages/openapi-plugin/src/lib/QraftCommand.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ export class QraftCommand extends QraftCommandBase<OpenAPIQraftCommandActionOpti
7171
'Use OpenAPI Operation `endpoint[<index>]` path part (e.g.: "/0/1/2") or `tags` as the base name of the service.',
7272
'endpoint[0]'
7373
)
74+
.option(
75+
'--root-security',
76+
'Use root-level OpenAPI security as the default for operations without their own security. Operation-level security overrides it according to OpenAPI semantics.'
77+
)
7478
.addOption(createRedoclyOption());
7579
}
7680

@@ -147,6 +151,7 @@ export class QraftCommand extends QraftCommandBase<OpenAPIQraftCommandActionOpti
147151
let services = getServices(schema as OpenAPISchemaType, {
148152
postfixServices: args.postfixServices,
149153
serviceNameBase: args.serviceNameBase,
154+
rootSecurity: args.rootSecurity,
150155
});
151156

152157
if (args.operationNameModifier) {

packages/openapi-plugin/src/lib/open-api/OpenAPISchemaType.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type OpenAPISchemaType = {
44
title: string;
55
version: string;
66
};
7+
security?: Array<Record<string, string[] | undefined>>;
78
paths: {
89
[path: string]: {
910
[method: string]: {
@@ -23,7 +24,7 @@ export type OpenAPISchemaType = {
2324
required?: boolean;
2425
};
2526
responses: {
26-
[statusCode in number | 'default']: {
27+
[statusCode in number | 'default']?: {
2728
$ref?: string;
2829
description?: string;
2930
content?: {

packages/openapi-plugin/src/lib/open-api/getServices.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { OpenAPISchemaType } from './OpenAPISchemaType.js';
12
import openAPI from '@openapi-qraft/test-fixtures/openapi.json' with { type: 'json' };
23
import { describe, expect, it } from 'vitest';
34
import { filterDocumentPaths } from '../filterDocumentPaths.js';
@@ -31,4 +32,126 @@ describe('getServices', () => {
3132
it('matches snapshot with "serviceNameBase: tags"', () => {
3233
expect(getServices(openAPI, { serviceNameBase: 'tags' })).toMatchSnapshot();
3334
});
35+
36+
it('inherits root-level security when `rootSecurity` is enabled and operation-level security is omitted', () => {
37+
const document: OpenAPISchemaType = {
38+
openapi: '3.1.0',
39+
info: {
40+
title: 'Fixture API',
41+
version: '1.0.0',
42+
},
43+
security: [{ jwtUserToken: [] }],
44+
paths: {
45+
'/accounts': {
46+
get: {
47+
operationId: 'getAccounts',
48+
responses: {
49+
200: {
50+
description: 'Successful response',
51+
},
52+
},
53+
},
54+
},
55+
},
56+
components: {
57+
parameters: undefined,
58+
},
59+
};
60+
61+
expect(
62+
getServices(document, { rootSecurity: true })[0]?.operations[0]?.security
63+
).toEqual([{ jwtUserToken: [] }]);
64+
});
65+
66+
it('does not inherit root-level security when `rootSecurity` is disabled', () => {
67+
const document: OpenAPISchemaType = {
68+
openapi: '3.1.0',
69+
info: {
70+
title: 'Fixture API',
71+
version: '1.0.0',
72+
},
73+
security: [{ jwtUserToken: [] }],
74+
paths: {
75+
'/accounts': {
76+
get: {
77+
operationId: 'getAccounts',
78+
responses: {
79+
200: {
80+
description: 'Successful response',
81+
},
82+
},
83+
},
84+
},
85+
},
86+
components: {
87+
parameters: undefined,
88+
},
89+
};
90+
91+
expect(
92+
getServices(document, { rootSecurity: false })[0]?.operations[0]?.security
93+
).toBeUndefined();
94+
});
95+
96+
it('prefers operation-level security over root-level security when `rootSecurity` is enabled', () => {
97+
const document: OpenAPISchemaType = {
98+
openapi: '3.1.0',
99+
info: {
100+
title: 'Fixture API',
101+
version: '1.0.0',
102+
},
103+
security: [{ jwtUserToken: [] }],
104+
paths: {
105+
'/accounts': {
106+
get: {
107+
operationId: 'getAccounts',
108+
security: [{ apiKey: [] }],
109+
responses: {
110+
200: {
111+
description: 'Successful response',
112+
},
113+
},
114+
},
115+
},
116+
},
117+
components: {
118+
parameters: undefined,
119+
},
120+
};
121+
122+
expect(
123+
getServices(document, { rootSecurity: true })[0]?.operations[0]?.security
124+
).toEqual([{ apiKey: [] }]);
125+
});
126+
127+
it('treats empty operation-level security as an explicit override when `rootSecurity` is enabled', () => {
128+
const document: OpenAPISchemaType = {
129+
openapi: '3.1.0',
130+
info: {
131+
title: 'Fixture API',
132+
version: '1.0.0',
133+
},
134+
security: [{ jwtUserToken: [] }],
135+
paths: {
136+
'/health': {
137+
get: {
138+
operationId: 'getHealth',
139+
security: [],
140+
responses: {
141+
200: {
142+
description: 'Successful response',
143+
},
144+
},
145+
},
146+
},
147+
},
148+
components: {
149+
parameters: undefined,
150+
},
151+
};
152+
153+
expect(
154+
getServices(document, { rootSecurity: true })[0]?.operations[0]?.security
155+
).toEqual([]);
156+
});
34157
});

packages/openapi-plugin/src/lib/open-api/getServices.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ export type ServiceOperation = ServiceOperationStable;
2929
export interface ServiceOutputOptions {
3030
postfixServices?: string; // todo::rename to `postfixService`
3131
serviceNameBase?: ServiceBaseName;
32+
rootSecurity?: boolean;
3233
}
3334

3435
export const getServices = (
3536
openApiJson: OpenAPISchemaType,
3637
{
3738
postfixServices = 'Service',
3839
serviceNameBase = 'endpoint[0]',
40+
rootSecurity,
3941
}: ServiceOutputOptions = {}
4042
) => {
4143
const paths = openApiJson.paths;
@@ -56,6 +58,11 @@ export const getServices = (
5658
}
5759

5860
const methodOperation = paths[path][method];
61+
const operationSecurity = resolveOperationSecurity(
62+
openApiJson.security,
63+
methodOperation,
64+
rootSecurity
65+
);
5966

6067
const { success, errors } = Object.entries(
6168
methodOperation.responses
@@ -66,6 +73,8 @@ export const getServices = (
6673
>
6774
>(
6875
(acc, [statusCode, response]) => {
76+
if (!response) return acc;
77+
6978
if (response.$ref) {
7079
response = resolveDocumentLocalRef(
7180
response.$ref,
@@ -145,7 +154,7 @@ export const getServices = (
145154
)
146155
: methodOperation.requestBody) ?? undefined,
147156
success: success,
148-
security: methodOperation.security,
157+
security: operationSecurity,
149158
});
150159
}
151160
}
@@ -154,6 +163,18 @@ export const getServices = (
154163
return Array.from(services.values());
155164
};
156165

166+
const resolveOperationSecurity = (
167+
rootSecurity: OpenAPISchemaType['security'],
168+
methodOperation: OpenAPISchemaType['paths'][string][string],
169+
shouldUseRootSecurity?: boolean
170+
) => {
171+
if (shouldUseRootSecurity && !('security' in methodOperation)) {
172+
return rootSecurity;
173+
}
174+
175+
return methodOperation.security;
176+
};
177+
157178
export const supportedMethod = (
158179
method: unknown
159180
): method is (typeof supportedHTTPMethods)[number] =>

0 commit comments

Comments
 (0)