fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3415 from hey-api/copilot/support-bulk-callback-schemas

feat: add bulk callback and async support for patch.schemas

authored by

Lubos and committed by
GitHub
714413e7 1331bd28

+462 -20
+6
.changeset/cold-buckets-fetch.md
··· 1 + --- 2 + "@hey-api/shared": patch 3 + "@hey-api/openapi-ts": patch 4 + --- 5 + 6 + **parser(patch)**: support callback for `patch.schemas`
-1
.github/workflows/pullfrog.yml
··· 43 43 GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} 44 44 DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} 45 45 OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} 46 -
+42 -9
packages/shared/src/config/parser/patch.ts
··· 125 125 * use cases include fixing incorrect data types, removing unwanted 126 126 * properties, adding missing fields, or standardizing date/time formats. 127 127 * 128 + * Can be: 129 + * - `Record<string, fn>`: Patch specific named schemas 130 + * - `function`: Bulk callback receives `(name, schema)` for every schema 131 + * 132 + * Both patterns support async functions for operations like fetching data 133 + * from external sources or performing I/O. 134 + * 128 135 * @example 129 136 * ```js 137 + * // Named schemas 130 138 * schemas: { 131 139 * Foo: (schema) => { 132 140 * // convert date-time format to timestamp ··· 146 154 * delete schema.properties.internalField; 147 155 * } 148 156 * } 157 + * 158 + * // Bulk callback for all schemas 159 + * schemas: (name, schema) => { 160 + * const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 161 + * if (match) { 162 + * schema.description = (schema.description || '') + 163 + * `\n@version ${match[1]}.${match[2]}.${match[3]}`; 164 + * } 165 + * } 166 + * 167 + * // Async example - fetch metadata from external source 168 + * schemas: async (name, schema) => { 169 + * const metadata = await fetchSchemaMetadata(name); 170 + * if (metadata) { 171 + * schema.description = `${schema.description}\n\n${metadata.notes}`; 172 + * } 173 + * } 149 174 * ``` 150 175 */ 151 - schemas?: Record< 152 - string, 153 - ( 154 - schema: 155 - | OpenApiSchemaObject.V2_0_X 156 - | OpenApiSchemaObject.V3_0_X 157 - | OpenApiSchemaObject.V3_1_X, 158 - ) => void 159 - >; 176 + schemas?: 177 + | Record< 178 + string, 179 + ( 180 + schema: 181 + | OpenApiSchemaObject.V2_0_X 182 + | OpenApiSchemaObject.V3_0_X 183 + | OpenApiSchemaObject.V3_1_X, 184 + ) => void | Promise<void> 185 + > 186 + | (( 187 + name: string, 188 + schema: 189 + | OpenApiSchemaObject.V2_0_X 190 + | OpenApiSchemaObject.V3_0_X 191 + | OpenApiSchemaObject.V3_1_X, 192 + ) => void | Promise<void>); 160 193 /** 161 194 * Patch the OpenAPI version string. The function receives the current version and should return the new version string. 162 195 * Useful for normalizing or overriding the version value before further processing.
+388
packages/shared/src/openApi/shared/utils/__tests__/patch.test.ts
··· 857 857 expect(versionFn).toHaveBeenCalledOnce(); 858 858 expect(spec.openapi).toBe('patched-3.1.0'); 859 859 }); 860 + 861 + it('calls bulk callback function for all schemas', async () => { 862 + const fn = vi.fn(); 863 + 864 + const spec: OpenApi.V3_1_X = { 865 + ...specMetadataV3, 866 + components: { 867 + schemas: { 868 + Bar: { 869 + type: 'object', 870 + }, 871 + Foo: { 872 + type: 'string', 873 + }, 874 + Qux: { 875 + type: 'number', 876 + }, 877 + }, 878 + }, 879 + }; 880 + 881 + await patchOpenApiSpec({ 882 + patchOptions: { 883 + schemas: fn, 884 + }, 885 + spec, 886 + }); 887 + 888 + expect(fn).toHaveBeenCalledTimes(3); 889 + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); 890 + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); 891 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); 892 + }); 893 + 894 + it('bulk callback mutates all schemas', async () => { 895 + const spec: OpenApi.V3_1_X = { 896 + ...specMetadataV3, 897 + components: { 898 + schemas: { 899 + Bar: { 900 + description: 'Bar schema', 901 + type: 'object', 902 + }, 903 + Foo: { 904 + description: 'Foo schema', 905 + type: 'string', 906 + }, 907 + }, 908 + }, 909 + }; 910 + 911 + await patchOpenApiSpec({ 912 + patchOptions: { 913 + schemas: (name, schema) => { 914 + schema.description = `${schema.description} - patched`; 915 + }, 916 + }, 917 + spec, 918 + }); 919 + 920 + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - patched'); 921 + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - patched'); 922 + }); 923 + 924 + it('bulk callback can extract version from schema name', async () => { 925 + const spec: OpenApi.V3_1_X = { 926 + ...specMetadataV3, 927 + components: { 928 + schemas: { 929 + OtherSchema: { 930 + type: 'string', 931 + }, 932 + ServiceRoot_v1_20_0_ServiceRoot: { 933 + description: 'Service root', 934 + type: 'object', 935 + }, 936 + User_v2_3_1_User: { 937 + description: 'User object', 938 + type: 'object', 939 + }, 940 + }, 941 + }, 942 + }; 943 + 944 + await patchOpenApiSpec({ 945 + patchOptions: { 946 + schemas: (name, schema) => { 947 + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 948 + if (match) { 949 + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; 950 + } 951 + }, 952 + }, 953 + spec, 954 + }); 955 + 956 + expect(spec.components?.schemas?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( 957 + 'Service root\n@version 1.20.0', 958 + ); 959 + expect(spec.components?.schemas?.User_v2_3_1_User!.description).toBe( 960 + 'User object\n@version 2.3.1', 961 + ); 962 + expect(spec.components?.schemas?.OtherSchema!.description).toBeUndefined(); 963 + }); 964 + 965 + it('bulk callback skips invalid schemas', async () => { 966 + const fn = vi.fn(); 967 + 968 + const spec: OpenApi.V3_1_X = { 969 + ...specMetadataV3, 970 + components: { 971 + schemas: { 972 + Bar: 123 as any, 973 + Baz: 'invalid' as any, 974 + Foo: null as any, 975 + Qux: { 976 + type: 'string', 977 + }, 978 + }, 979 + }, 980 + }; 981 + 982 + await patchOpenApiSpec({ 983 + patchOptions: { 984 + schemas: fn, 985 + }, 986 + spec, 987 + }); 988 + 989 + expect(fn).toHaveBeenCalledOnce(); 990 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); 991 + }); 992 + 993 + it('supports async bulk callback', async () => { 994 + const spec: OpenApi.V3_1_X = { 995 + ...specMetadataV3, 996 + components: { 997 + schemas: { 998 + Bar: { 999 + description: 'Bar schema', 1000 + type: 'object', 1001 + }, 1002 + Foo: { 1003 + description: 'Foo schema', 1004 + type: 'string', 1005 + }, 1006 + }, 1007 + }, 1008 + }; 1009 + 1010 + await patchOpenApiSpec({ 1011 + patchOptions: { 1012 + schemas: async (name, schema) => { 1013 + // Simulate async operation 1014 + await Promise.resolve(); 1015 + schema.description = `${schema.description} - async patched`; 1016 + }, 1017 + }, 1018 + spec, 1019 + }); 1020 + 1021 + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - async patched'); 1022 + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - async patched'); 1023 + }); 1024 + 1025 + it('supports async Record-based callbacks', async () => { 1026 + const spec: OpenApi.V3_1_X = { 1027 + ...specMetadataV3, 1028 + components: { 1029 + schemas: { 1030 + Bar: { 1031 + description: 'Bar schema', 1032 + type: 'object', 1033 + }, 1034 + Foo: { 1035 + description: 'Foo schema', 1036 + type: 'string', 1037 + }, 1038 + }, 1039 + }, 1040 + }; 1041 + 1042 + await patchOpenApiSpec({ 1043 + patchOptions: { 1044 + schemas: { 1045 + Bar: async (schema) => { 1046 + await Promise.resolve(); 1047 + schema.description = `${schema.description} - async`; 1048 + }, 1049 + Foo: async (schema) => { 1050 + await Promise.resolve(); 1051 + schema.description = `${schema.description} - async`; 1052 + }, 1053 + }, 1054 + }, 1055 + spec, 1056 + }); 1057 + 1058 + expect(spec.components?.schemas?.Bar!.description).toBe('Bar schema - async'); 1059 + expect(spec.components?.schemas?.Foo!.description).toBe('Foo schema - async'); 1060 + }); 860 1061 }); 861 1062 862 1063 describe('OpenAPI v2', () => { ··· 1047 1248 }); 1048 1249 expect(versionFn).toHaveBeenCalledOnce(); 1049 1250 expect(spec.swagger).toBe('patched-2.0'); 1251 + }); 1252 + 1253 + it('calls bulk callback function for all schemas', async () => { 1254 + const fn = vi.fn(); 1255 + 1256 + const spec: OpenApi.V2_0_X = { 1257 + ...specMetadataV2, 1258 + definitions: { 1259 + Bar: { 1260 + type: 'object', 1261 + }, 1262 + Foo: { 1263 + type: 'string', 1264 + }, 1265 + Qux: { 1266 + type: 'number', 1267 + }, 1268 + }, 1269 + }; 1270 + 1271 + await patchOpenApiSpec({ 1272 + patchOptions: { 1273 + schemas: fn, 1274 + }, 1275 + spec, 1276 + }); 1277 + 1278 + expect(fn).toHaveBeenCalledTimes(3); 1279 + expect(fn).toHaveBeenCalledWith('Bar', { type: 'object' }); 1280 + expect(fn).toHaveBeenCalledWith('Foo', { type: 'string' }); 1281 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'number' }); 1282 + }); 1283 + 1284 + it('bulk callback mutates all schemas', async () => { 1285 + const spec: OpenApi.V2_0_X = { 1286 + ...specMetadataV2, 1287 + definitions: { 1288 + Bar: { 1289 + description: 'Bar schema', 1290 + type: 'object', 1291 + }, 1292 + Foo: { 1293 + description: 'Foo schema', 1294 + type: 'string', 1295 + }, 1296 + }, 1297 + }; 1298 + 1299 + await patchOpenApiSpec({ 1300 + patchOptions: { 1301 + schemas: (name, schema) => { 1302 + schema.description = `${schema.description} - patched`; 1303 + }, 1304 + }, 1305 + spec, 1306 + }); 1307 + 1308 + expect(spec.definitions?.Bar!.description).toBe('Bar schema - patched'); 1309 + expect(spec.definitions?.Foo!.description).toBe('Foo schema - patched'); 1310 + }); 1311 + 1312 + it('bulk callback can extract version from schema name', async () => { 1313 + const spec: OpenApi.V2_0_X = { 1314 + ...specMetadataV2, 1315 + definitions: { 1316 + OtherSchema: { 1317 + type: 'string', 1318 + }, 1319 + ServiceRoot_v1_20_0_ServiceRoot: { 1320 + description: 'Service root', 1321 + type: 'object', 1322 + }, 1323 + User_v2_3_1_User: { 1324 + description: 'User object', 1325 + type: 'object', 1326 + }, 1327 + }, 1328 + }; 1329 + 1330 + await patchOpenApiSpec({ 1331 + patchOptions: { 1332 + schemas: (name, schema) => { 1333 + const match = name.match(/_v(\d+)_(\d+)_(\d+)_/); 1334 + if (match) { 1335 + schema.description = `${schema.description || ''}\n@version ${match[1]}.${match[2]}.${match[3]}`; 1336 + } 1337 + }, 1338 + }, 1339 + spec, 1340 + }); 1341 + 1342 + expect(spec.definitions?.ServiceRoot_v1_20_0_ServiceRoot!.description).toBe( 1343 + 'Service root\n@version 1.20.0', 1344 + ); 1345 + expect(spec.definitions?.User_v2_3_1_User!.description).toBe('User object\n@version 2.3.1'); 1346 + expect(spec.definitions?.OtherSchema!.description).toBeUndefined(); 1347 + }); 1348 + 1349 + it('bulk callback skips invalid schemas', async () => { 1350 + const fn = vi.fn(); 1351 + 1352 + const spec: OpenApi.V2_0_X = { 1353 + ...specMetadataV2, 1354 + definitions: { 1355 + Bar: 123 as any, 1356 + Baz: 'invalid' as any, 1357 + Foo: null as any, 1358 + Qux: { 1359 + type: 'string', 1360 + }, 1361 + }, 1362 + }; 1363 + 1364 + await patchOpenApiSpec({ 1365 + patchOptions: { 1366 + schemas: fn, 1367 + }, 1368 + spec, 1369 + }); 1370 + 1371 + expect(fn).toHaveBeenCalledOnce(); 1372 + expect(fn).toHaveBeenCalledWith('Qux', { type: 'string' }); 1373 + }); 1374 + 1375 + it('supports async bulk callback', async () => { 1376 + const spec: OpenApi.V2_0_X = { 1377 + ...specMetadataV2, 1378 + definitions: { 1379 + Bar: { 1380 + description: 'Bar schema', 1381 + type: 'object', 1382 + }, 1383 + Foo: { 1384 + description: 'Foo schema', 1385 + type: 'string', 1386 + }, 1387 + }, 1388 + }; 1389 + 1390 + await patchOpenApiSpec({ 1391 + patchOptions: { 1392 + schemas: async (name, schema) => { 1393 + // Simulate async operation 1394 + await Promise.resolve(); 1395 + schema.description = `${schema.description} - async patched`; 1396 + }, 1397 + }, 1398 + spec, 1399 + }); 1400 + 1401 + expect(spec.definitions?.Bar!.description).toBe('Bar schema - async patched'); 1402 + expect(spec.definitions?.Foo!.description).toBe('Foo schema - async patched'); 1403 + }); 1404 + 1405 + it('supports async Record-based callbacks', async () => { 1406 + const spec: OpenApi.V2_0_X = { 1407 + ...specMetadataV2, 1408 + definitions: { 1409 + Bar: { 1410 + description: 'Bar schema', 1411 + type: 'object', 1412 + }, 1413 + Foo: { 1414 + description: 'Foo schema', 1415 + type: 'string', 1416 + }, 1417 + }, 1418 + }; 1419 + 1420 + await patchOpenApiSpec({ 1421 + patchOptions: { 1422 + schemas: { 1423 + Bar: async (schema) => { 1424 + await Promise.resolve(); 1425 + schema.description = `${schema.description} - async`; 1426 + }, 1427 + Foo: async (schema) => { 1428 + await Promise.resolve(); 1429 + schema.description = `${schema.description} - async`; 1430 + }, 1431 + }, 1432 + }, 1433 + spec, 1434 + }); 1435 + 1436 + expect(spec.definitions?.Bar!.description).toBe('Bar schema - async'); 1437 + expect(spec.definitions?.Foo!.description).toBe('Foo schema - async'); 1050 1438 }); 1051 1439 }); 1052 1440
+26 -10
packages/shared/src/openApi/shared/utils/patch.ts
··· 37 37 } 38 38 39 39 if (patchOptions.schemas && spec.definitions) { 40 - for (const key in patchOptions.schemas) { 41 - const schema = spec.definitions[key]; 42 - if (!schema || typeof schema !== 'object') continue; 40 + if (typeof patchOptions.schemas === 'function') { 41 + for (const [key, schema] of Object.entries(spec.definitions)) { 42 + if (schema && typeof schema === 'object') { 43 + await patchOptions.schemas(key, schema); 44 + } 45 + } 46 + } else { 47 + for (const key in patchOptions.schemas) { 48 + const schema = spec.definitions[key]; 49 + if (!schema || typeof schema !== 'object') continue; 43 50 44 - const patchFn = patchOptions.schemas[key]!; 45 - patchFn(schema); 51 + const patchFn = patchOptions.schemas[key]!; 52 + await patchFn(schema); 53 + } 46 54 } 47 55 } 48 56 ··· 80 88 81 89 if (spec.components) { 82 90 if (patchOptions.schemas && spec.components.schemas) { 83 - for (const key in patchOptions.schemas) { 84 - const schema = spec.components.schemas[key]; 85 - if (!schema || typeof schema !== 'object') continue; 91 + if (typeof patchOptions.schemas === 'function') { 92 + for (const [key, schema] of Object.entries(spec.components.schemas)) { 93 + if (schema && typeof schema === 'object') { 94 + await patchOptions.schemas(key, schema as Parameters<typeof patchOptions.schemas>[1]); 95 + } 96 + } 97 + } else { 98 + for (const key in patchOptions.schemas) { 99 + const schema = spec.components.schemas[key]; 100 + if (!schema || typeof schema !== 'object') continue; 86 101 87 - const patchFn = patchOptions.schemas[key]!; 88 - patchFn(schema as Parameters<typeof patchFn>[0]); 102 + const patchFn = patchOptions.schemas[key]!; 103 + await patchFn(schema as Parameters<typeof patchFn>[0]); 104 + } 89 105 } 90 106 } 91 107