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]); + } } }