From 652daa1ca401ab041e52122fa914ae5e4020027d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:43:47 +0000 Subject: [PATCH 1/6] Initial plan -- 2.51.2 From c281262bf1a108724e278336218f4788eeb86005 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:51:03 +0000 Subject: [PATCH 2/6] feat: support bulk callback for patch.schemas - Updated type definition to allow function signature in addition to Record - Implemented bulk callback for OpenAPI v3 schemas - Implemented bulk callback for OpenAPI v2 (swagger) definitions - Added comprehensive tests for both v2 and v3 specs - Tests cover basic callbacks, mutations, version extraction, and invalid schema handling Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/shared/src/config/parser/patch.ts | 40 ++- .../shared/utils/__tests__/patch.test.ts | 254 ++++++++++++++++++ .../shared/src/openApi/shared/utils/patch.ts | 40 ++- 3 files changed, 313 insertions(+), 21 deletions(-) diff --git a/packages/shared/src/config/parser/patch.ts b/packages/shared/src/config/parser/patch.ts index e11bb0713..2e207e50b 100644 --- a/packages/shared/src/config/parser/patch.ts +++ b/packages/shared/src/config/parser/patch.ts @@ -125,8 +125,13 @@ export type Patch = * use cases include fixing incorrect data types, removing unwanted * properties, adding missing fields, or standardizing date/time formats. * + * Can be: + * - `Record`: Patch specific named schemas + * - `function`: Bulk callback receives `(name, schema)` for every schema + * * @example * ```js + * // Named schemas * schemas: { * Foo: (schema) => { * // convert date-time format to timestamp @@ -146,17 +151,34 @@ export type Patch = * delete schema.properties.internalField; * } * } + * + * // Bulk callback for all schemas + * schemas: (name, schema) => { + * const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); + * if (match) { + * schema.description = (schema.description || '') + + * `\n@version ${match[1]}.${match[2]}.${match[3]}`; + * } + * } * ``` */ - schemas?: Record< - string, - ( - schema: - | OpenApiSchemaObject.V2_0_X - | OpenApiSchemaObject.V3_0_X - | OpenApiSchemaObject.V3_1_X, - ) => void - >; + schemas?: + | Record< + string, + ( + schema: + | OpenApiSchemaObject.V2_0_X + | OpenApiSchemaObject.V3_0_X + | OpenApiSchemaObject.V3_1_X, + ) => void + > + | (( + name: string, + schema: + | OpenApiSchemaObject.V2_0_X + | OpenApiSchemaObject.V3_0_X + | OpenApiSchemaObject.V3_1_X, + ) => void); /** * Patch the OpenAPI version string. The function receives the current version and should return the new version string. * Useful for normalizing or overriding the version value before further processing. diff --git a/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts b/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts index c9dfbe850..bb2ba2e50 100644 --- a/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts +++ b/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts @@ -857,6 +857,138 @@ describe('patchOpenApiSpec', () => { expect(versionFn).toHaveBeenCalledOnce(); expect(spec.openapi).toBe('patched-3.1.0'); }); + + it('calls bulk callback function for all schemas', async () => { + const fn = vi.fn(); + + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + Bar: { + type: 'object', + }, + Foo: { + type: 'string', + }, + Qux: { + type: 'number', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: fn, + }, + spec, + }); + + expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); + }); + + it('bulk callback mutates all schemas', async () => { + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: (name, schema) => { + schema.description = `${schema.description} - patched`; + }, + }, + spec, + }); + + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - patched'); + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - patched'); + }); + + it('bulk callback can extract version from schema name', async () => { + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + OtherSchema: { + type: 'string', + }, + ServiceRoot_v1_20_0_ServiceRoot: { + description: 'Service root', + type: 'object', + }, + User_v2_3_1_User: { + description: 'User object', + type: 'object', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: (name, schema) => { + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); + if (match) { + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; + } + }, + }, + spec, + }); + + expect(spec.components?.schemas?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( + 'Service root\n@version 1.20.0', + ); + expect(spec.components?.schemas?.User_v2_3_1_User!.description).toBe( + 'User object\n@version 2.3.1', + ); + expect(spec.components?.schemas?.OtherSchema!.description).toBeUndefined(); + }); + + it('bulk callback skips invalid schemas', async () => { + const fn = vi.fn(); + + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + Bar: 123 as any, + Baz: 'invalid' as any, + Foo: null as any, + Qux: { + type: 'string', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: fn, + }, + spec, + }); + + expect(fn).toHaveBeenCalledOnce(); + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); + }); }); describe('OpenAPI v2', () => { @@ -1048,6 +1180,128 @@ describe('patchOpenApiSpec', () => { expect(versionFn).toHaveBeenCalledOnce(); expect(spec.swagger).toBe('patched-2.0'); }); + + it('calls bulk callback function for all schemas', async () => { + const fn = vi.fn(); + + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + Bar: { + type: 'object', + }, + Foo: { + type: 'string', + }, + Qux: { + type: 'number', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: fn, + }, + spec, + }); + + expect(fn).toHaveBeenCalledTimes(3); + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); + }); + + it('bulk callback mutates all schemas', async () => { + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: (name, schema) => { + schema.description = `${schema.description} - patched`; + }, + }, + spec, + }); + + expect(spec.definitions?.Bar!.description).toBe('Bar schema - patched'); + expect(spec.definitions?.Foo!.description).toBe('Foo schema - patched'); + }); + + it('bulk callback can extract version from schema name', async () => { + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + OtherSchema: { + type: 'string', + }, + ServiceRoot_v1_20_0_ServiceRoot: { + description: 'Service root', + type: 'object', + }, + User_v2_3_1_User: { + description: 'User object', + type: 'object', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: (name, schema) => { + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); + if (match) { + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; + } + }, + }, + spec, + }); + + expect(spec.definitions?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( + 'Service root\n@version 1.20.0', + ); + expect(spec.definitions?.User_v2_3_1_User!.description).toBe('User object\n@version 2.3.1'); + expect(spec.definitions?.OtherSchema!.description).toBeUndefined(); + }); + + it('bulk callback skips invalid schemas', async () => { + const fn = vi.fn(); + + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + Bar: 123 as any, + Baz: 'invalid' as any, + Foo: null as any, + Qux: { + type: 'string', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: fn, + }, + spec, + }); + + expect(fn).toHaveBeenCalledOnce(); + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); + }); }); describe('real-world usage', () => { diff --git a/packages/shared/src/openApi/shared/utils/patch.ts b/packages/shared/src/openApi/shared/utils/patch.ts index 9a209e036..40bd66120 100644 --- a/packages/shared/src/openApi/shared/utils/patch.ts +++ b/packages/shared/src/openApi/shared/utils/patch.ts @@ -37,12 +37,20 @@ export async function patchOpenApiSpec({ } if (patchOptions.schemas && spec.definitions) { - for (const key in patchOptions.schemas) { - const schema = spec.definitions[key]; - if (!schema || typeof schema !== 'object') continue; - - const patchFn = patchOptions.schemas[key]!; - patchFn(schema); + if (typeof patchOptions.schemas === 'function') { + for (const [key, schema] of Object.entries(spec.definitions)) { + if (schema && typeof schema === 'object') { + patchOptions.schemas(key, schema); + } + } + } else { + for (const key in patchOptions.schemas) { + const schema = spec.definitions[key]; + if (!schema || typeof schema !== 'object') continue; + + const patchFn = patchOptions.schemas[key]!; + patchFn(schema); + } } } @@ -80,12 +88,20 @@ export async function patchOpenApiSpec({ if (spec.components) { if (patchOptions.schemas && spec.components.schemas) { - for (const key in patchOptions.schemas) { - const schema = spec.components.schemas[key]; - if (!schema || typeof schema !== 'object') continue; - - const patchFn = patchOptions.schemas[key]!; - patchFn(schema as Parameters[0]); + if (typeof patchOptions.schemas === 'function') { + for (const [key, schema] of Object.entries(spec.components.schemas)) { + if (schema && typeof schema === 'object') { + patchOptions.schemas(key, schema as Parameters[1]); + } + } + } else { + for (const key in patchOptions.schemas) { + const schema = spec.components.schemas[key]; + if (!schema || typeof schema !== 'object') continue; + + const patchFn = patchOptions.schemas[key]!; + patchFn(schema as Parameters[0]); + } } } -- 2.51.2 From 987ecdc8e89592c5e1d30bee865ad6d1d7f30e57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 21:52:54 +0000 Subject: [PATCH 3/6] docs: update plan with manual verification complete Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .github/workflows/pullfrog.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pullfrog.yml b/.github/workflows/pullfrog.yml index d86479432..e5a760711 100644 --- a/.github/workflows/pullfrog.yml +++ b/.github/workflows/pullfrog.yml @@ -43,4 +43,3 @@ jobs: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} - -- 2.51.2 From ba8b0c835f4092773bd270c828b49d33f23894cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:14:13 +0000 Subject: [PATCH 4/6] feat: make patch.schemas callbacks async-compatible - Updated type definition to allow Promise return type for both Record and bulk callback - Updated implementation to await all schema callback invocations (v2 and v3) - Added comprehensive tests for async bulk callbacks and async Record-based callbacks - Verified with manual CLI test that async operations work correctly Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/shared/src/config/parser/patch.ts | 4 +- .../shared/utils/__tests__/patch.test.ts | 134 ++++++++++++++++++ .../shared/src/openApi/shared/utils/patch.ts | 8 +- 3 files changed, 140 insertions(+), 6 deletions(-) diff --git a/packages/shared/src/config/parser/patch.ts b/packages/shared/src/config/parser/patch.ts index 2e207e50b..9ae6e58a4 100644 --- a/packages/shared/src/config/parser/patch.ts +++ b/packages/shared/src/config/parser/patch.ts @@ -170,7 +170,7 @@ export type Patch = | OpenApiSchemaObject.V2_0_X | OpenApiSchemaObject.V3_0_X | OpenApiSchemaObject.V3_1_X, - ) => void + ) => void | Promise > | (( name: string, @@ -178,7 +178,7 @@ export type Patch = | OpenApiSchemaObject.V2_0_X | OpenApiSchemaObject.V3_0_X | OpenApiSchemaObject.V3_1_X, - ) => void); + ) => void | Promise); /** * Patch the OpenAPI version string. The function receives the current version and should return the new version string. * Useful for normalizing or overriding the version value before further processing. diff --git a/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts b/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts index bb2ba2e50..2eb760822 100644 --- a/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts +++ b/packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts @@ -989,6 +989,75 @@ describe('patchOpenApiSpec', () => { expect(fn).toHaveBeenCalledOnce(); expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); }); + + it('supports async bulk callback', async () => { + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: async (name, schema) => { + // Simulate async operation + await Promise.resolve(); + schema.description = `${schema.description} - async patched`; + }, + }, + spec, + }); + + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - async patched'); + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - async patched'); + }); + + it('supports async Record-based callbacks', async () => { + const spec: OpenApi.V3_1_X = { + ...specMetadataV3, + components: { + schemas: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: { + Bar: async (schema) => { + await Promise.resolve(); + schema.description = `${schema.description} - async`; + }, + Foo: async (schema) => { + await Promise.resolve(); + schema.description = `${schema.description} - async`; + }, + }, + }, + spec, + }); + + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - async'); + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - async'); + }); }); describe('OpenAPI v2', () => { @@ -1302,6 +1371,71 @@ describe('patchOpenApiSpec', () => { expect(fn).toHaveBeenCalledOnce(); expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); }); + + it('supports async bulk callback', async () => { + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: async (name, schema) => { + // Simulate async operation + await Promise.resolve(); + schema.description = `${schema.description} - async patched`; + }, + }, + spec, + }); + + expect(spec.definitions?.Bar!.description).toBe('Bar schema - async patched'); + expect(spec.definitions?.Foo!.description).toBe('Foo schema - async patched'); + }); + + it('supports async Record-based callbacks', async () => { + const spec: OpenApi.V2_0_X = { + ...specMetadataV2, + definitions: { + Bar: { + description: 'Bar schema', + type: 'object', + }, + Foo: { + description: 'Foo schema', + type: 'string', + }, + }, + }; + + await patchOpenApiSpec({ + patchOptions: { + schemas: { + Bar: async (schema) => { + await Promise.resolve(); + schema.description = `${schema.description} - async`; + }, + Foo: async (schema) => { + await Promise.resolve(); + schema.description = `${schema.description} - async`; + }, + }, + }, + spec, + }); + + expect(spec.definitions?.Bar!.description).toBe('Bar schema - async'); + expect(spec.definitions?.Foo!.description).toBe('Foo schema - async'); + }); }); describe('real-world usage', () => { diff --git a/packages/shared/src/openApi/shared/utils/patch.ts b/packages/shared/src/openApi/shared/utils/patch.ts index 40bd66120..9df60444c 100644 --- a/packages/shared/src/openApi/shared/utils/patch.ts +++ b/packages/shared/src/openApi/shared/utils/patch.ts @@ -40,7 +40,7 @@ export async function patchOpenApiSpec({ if (typeof patchOptions.schemas === 'function') { for (const [key, schema] of Object.entries(spec.definitions)) { if (schema && typeof schema === 'object') { - patchOptions.schemas(key, schema); + await patchOptions.schemas(key, schema); } } } else { @@ -49,7 +49,7 @@ export async function patchOpenApiSpec({ if (!schema || typeof schema !== 'object') continue; const patchFn = patchOptions.schemas[key]!; - patchFn(schema); + await patchFn(schema); } } } @@ -91,7 +91,7 @@ export async function patchOpenApiSpec({ if (typeof patchOptions.schemas === 'function') { for (const [key, schema] of Object.entries(spec.components.schemas)) { if (schema && typeof schema === 'object') { - patchOptions.schemas(key, schema as Parameters[1]); + await patchOptions.schemas(key, schema as Parameters[1]); } } } else { @@ -100,7 +100,7 @@ export async function patchOpenApiSpec({ if (!schema || typeof schema !== 'object') continue; const patchFn = patchOptions.schemas[key]!; - patchFn(schema as Parameters[0]); + await patchFn(schema as Parameters[0]); } } } -- 2.51.2 From 01e6bc4063a55c84c14ed3074a039c763229a773 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:15:56 +0000 Subject: [PATCH 5/6] docs: add async example to patch.schemas JSDoc Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- packages/shared/src/config/parser/patch.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/shared/src/config/parser/patch.ts b/packages/shared/src/config/parser/patch.ts index 9ae6e58a4..941405889 100644 --- a/packages/shared/src/config/parser/patch.ts +++ b/packages/shared/src/config/parser/patch.ts @@ -129,6 +129,9 @@ export type Patch = * - `Record`: Patch specific named schemas * - `function`: Bulk callback receives `(name, schema)` for every schema * + * Both patterns support async functions for operations like fetching data + * from external sources or performing I/O. + * * @example * ```js * // Named schemas @@ -160,6 +163,14 @@ export type Patch = * `\n@version ${match[1]}.${match[2]}.${match[3]}`; * } * } + * + * // Async example - fetch metadata from external source + * schemas: async (name, schema) => { + * const metadata = await fetchSchemaMetadata(name); + * if (metadata) { + * schema.description = `${schema.description}\n\n${metadata.notes}`; + * } + * } * ``` */ schemas?: -- 2.51.2 From e494f4dd828167a0096a0a488b222a013f911055 Mon Sep 17 00:00:00 2001 From: Lubos Date: Thu, 19 Feb 2026 06:37:32 +0800 Subject: [PATCH 6/6] Update patch.schemas to support bulk callback --- .changeset/cold-buckets-fetch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cold-buckets-fetch.md diff --git a/.changeset/cold-buckets-fetch.md b/.changeset/cold-buckets-fetch.md new file mode 100644 index 000000000..770e5b40c --- /dev/null +++ b/.changeset/cold-buckets-fetch.md @@ -0,0 +1,6 @@ +--- +"@hey-api/shared": patch +"@hey-api/openapi-ts": patch +--- + +**parser(patch)**: support callback for `patch.schemas` -- 2.51.2