From 36ce55f499c0907766539faeb7ea4363b31fb8d9 Mon Sep 17 00:00:00 2001 From: dracomithril Date: Tue, 19 Aug 2025 23:03:02 +0200 Subject: [PATCH 1/2] Reducing security schema duplicates in OpenAPI parser --- .../2.0.x/parser/__tests__/operation.test.ts | 93 +++++++++++++++++++ .../src/openApi/2.0.x/parser/operation.ts | 8 +- .../3.0.x/parser/__tests__/operation.test.ts | 86 +++++++++++++++++ .../src/openApi/3.0.x/parser/operation.ts | 8 +- .../3.1.x/parser/__tests__/operation.test.ts | 86 +++++++++++++++++ .../src/openApi/3.1.x/parser/operation.ts | 8 +- 6 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/operation.test.ts create mode 100644 packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/operation.test.ts create mode 100644 packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/operation.test.ts diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/operation.test.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/operation.test.ts new file mode 100644 index 000000000..98c3f6177 --- /dev/null +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/operation.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from 'vitest'; + +import type { IR } from '../../../../ir/types'; +import type { SecuritySchemeObject } from '../../types/spec'; +import { parseOperation } from '../operation'; + +type ParseOperationProps = Parameters[0]; + +describe('operation', () => { + const context = { + config: { + plugins: {}, + }, + ir: { + paths: {}, + servers: [], + }, + } as unknown as IR.Context; + + it('should parse operation correctly', () => { + const method = 'get'; + const operation: ParseOperationProps['operation'] = { + operationId: 'testOperation', + responses: {}, + security: [ + { + apiKeyAuth: [], + basicAuthRule: [], + }, + { + apiKeyAuth: [], + oauthRule: [], + }, + ], + summary: 'Test Operation', + }; + const path = '/test'; + const securitySchemesMap = new Map([ + ['apiKeyAuth', { in: 'header', name: 'Auth', type: 'apiKey' }], + ['basicAuthRule', { description: 'Basic Auth', type: 'basic' }], + [ + 'oauthRule', + { + description: 'OAuth2', + flow: 'password', + scopes: { + read: 'Grants read access', + write: 'Grants write access', + }, + tokenUrl: 'https://example.com/oauth/token', + type: 'oauth2', + }, + ], + ]); + const state: ParseOperationProps['state'] = { + ids: new Map(), + }; + + parseOperation({ + context, + method, + operation, + path, + securitySchemesMap, + state, + }); + + expect(context.ir.paths?.[path]?.[method]).toEqual({ + id: 'testOperation', + method, + operationId: 'testOperation', + path, + security: [ + { in: 'header', name: 'Auth', type: 'apiKey' }, + { description: 'Basic Auth', scheme: 'basic', type: 'http' }, + { + description: 'OAuth2', + flows: { + password: { + scopes: { + read: 'Grants read access', + write: 'Grants write access', + }, + tokenUrl: 'https://example.com/oauth/token', + }, + }, + type: 'oauth2', + }, + ], + summary: 'Test Operation', + }); + }); +}); diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts index dc3ef8c6e..fff682669 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts @@ -261,7 +261,7 @@ const operationToIrOperation = ({ } if (operation.security) { - const securitySchemeObjects: Array = []; + const securitySchemeObjects: Map = new Map(); for (const securityRequirementObject of operation.security) { for (const name in securityRequirementObject) { @@ -325,12 +325,12 @@ const operationToIrOperation = ({ continue; } - securitySchemeObjects.push(irSecuritySchemeObject); + securitySchemeObjects.set(name, irSecuritySchemeObject); } } - if (securitySchemeObjects.length) { - irOperation.security = securitySchemeObjects; + if (securitySchemeObjects.size) { + irOperation.security = Array.from(securitySchemeObjects.values()); } } diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/operation.test.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/operation.test.ts new file mode 100644 index 000000000..559512892 --- /dev/null +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/operation.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; + +import type { IR } from '../../../../ir/types'; +import type { SecuritySchemeObject } from '../../types/spec'; +import { parseOperation } from '../operation'; + +type ParseOperationProps = Parameters[0]; + +describe('operation', () => { + const context = { + config: { + plugins: {}, + }, + ir: { + paths: {}, + servers: [], + }, + } as unknown as IR.Context; + + it('should parse operation correctly', () => { + const method = 'get'; + const operation: ParseOperationProps['operation'] = { + operationId: 'testOperation', + responses: {}, + security: [ + { + apiKeyAuth: [], + }, + { + apiKeyAuth: [], + }, + { + oauthRule: ['read'], + }, + { + oauthRule: ['write'], + }, + ], + summary: 'Test Operation', + }; + const path = '/test'; + + const oauth2: SecuritySchemeObject = { + description: 'OAuth2', + flows: { + password: { + scopes: { + read: 'Grants read access', + write: 'Grants write access', + }, + tokenUrl: 'https://example.com/oauth/token', + }, + }, + type: 'oauth2', + }; + const securitySchemesMap = new Map([ + ['apiKeyAuth', { in: 'header', name: 'Auth', type: 'apiKey' }], + [ + 'basicAuthRule', + { description: 'Basic Auth', scheme: 'basic', type: 'http' }, + ], + ['oauthRule', oauth2], + ]); + const state: ParseOperationProps['state'] = { + ids: new Map(), + }; + + parseOperation({ + context, + method, + operation, + path, + securitySchemesMap, + state, + }); + + expect(context.ir.paths?.[path]?.[method]).toEqual({ + id: 'testOperation', + method, + operationId: 'testOperation', + path, + security: [{ in: 'header', name: 'Auth', type: 'apiKey' }, oauth2], + summary: 'Test Operation', + }); + }); +}); diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts index dff44c773..427d01364 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts @@ -203,7 +203,7 @@ const operationToIrOperation = ({ } if (operation.security) { - const securitySchemeObjects: Array = []; + const securitySchemeObjects: Map = new Map(); for (const securityRequirementObject of operation.security) { for (const name in securityRequirementObject) { @@ -213,12 +213,12 @@ const operationToIrOperation = ({ continue; } - securitySchemeObjects.push(securitySchemeObject); + securitySchemeObjects.set(name, securitySchemeObject); } } - if (securitySchemeObjects.length) { - irOperation.security = securitySchemeObjects; + if (securitySchemeObjects.size) { + irOperation.security = Array.from(securitySchemeObjects.values()); } } diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/operation.test.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/operation.test.ts new file mode 100644 index 000000000..559512892 --- /dev/null +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/operation.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; + +import type { IR } from '../../../../ir/types'; +import type { SecuritySchemeObject } from '../../types/spec'; +import { parseOperation } from '../operation'; + +type ParseOperationProps = Parameters[0]; + +describe('operation', () => { + const context = { + config: { + plugins: {}, + }, + ir: { + paths: {}, + servers: [], + }, + } as unknown as IR.Context; + + it('should parse operation correctly', () => { + const method = 'get'; + const operation: ParseOperationProps['operation'] = { + operationId: 'testOperation', + responses: {}, + security: [ + { + apiKeyAuth: [], + }, + { + apiKeyAuth: [], + }, + { + oauthRule: ['read'], + }, + { + oauthRule: ['write'], + }, + ], + summary: 'Test Operation', + }; + const path = '/test'; + + const oauth2: SecuritySchemeObject = { + description: 'OAuth2', + flows: { + password: { + scopes: { + read: 'Grants read access', + write: 'Grants write access', + }, + tokenUrl: 'https://example.com/oauth/token', + }, + }, + type: 'oauth2', + }; + const securitySchemesMap = new Map([ + ['apiKeyAuth', { in: 'header', name: 'Auth', type: 'apiKey' }], + [ + 'basicAuthRule', + { description: 'Basic Auth', scheme: 'basic', type: 'http' }, + ], + ['oauthRule', oauth2], + ]); + const state: ParseOperationProps['state'] = { + ids: new Map(), + }; + + parseOperation({ + context, + method, + operation, + path, + securitySchemesMap, + state, + }); + + expect(context.ir.paths?.[path]?.[method]).toEqual({ + id: 'testOperation', + method, + operationId: 'testOperation', + path, + security: [{ in: 'header', name: 'Auth', type: 'apiKey' }, oauth2], + summary: 'Test Operation', + }); + }); +}); diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts index 8d53dd8e2..11e45accc 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts @@ -188,7 +188,7 @@ const operationToIrOperation = ({ } if (operation.security) { - const securitySchemeObjects: Array = []; + const securitySchemeObjects: Map = new Map(); for (const securityRequirementObject of operation.security) { for (const name in securityRequirementObject) { @@ -198,12 +198,12 @@ const operationToIrOperation = ({ continue; } - securitySchemeObjects.push(securitySchemeObject); + securitySchemeObjects.set(name, securitySchemeObject); } } - if (securitySchemeObjects.length) { - irOperation.security = securitySchemeObjects; + if (securitySchemeObjects.size) { + irOperation.security = Array.from(securitySchemeObjects.values()); } } From 4038e1cef96c031acde97ccf82176a0fedbb3f8c Mon Sep 17 00:00:00 2001 From: Lubos Date: Wed, 20 Aug 2025 19:07:13 +0800 Subject: [PATCH 2/2] Create ninety-kings-confess.md --- .changeset/ninety-kings-confess.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ninety-kings-confess.md diff --git a/.changeset/ninety-kings-confess.md b/.changeset/ninety-kings-confess.md new file mode 100644 index 000000000..c12e22963 --- /dev/null +++ b/.changeset/ninety-kings-confess.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +fix(parser): deduplicate security schemas based on name