Skip to content

Commit 0d359e7

Browse files
authored
feat(typescript): Generate file upload section in README.md for multipart-form file uploads (#9986)
1 parent d7a50fb commit 0d359e7

File tree

48 files changed

+1491
-72
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1491
-72
lines changed

generators/typescript/sdk/generator/src/readme/ReadmeConfigBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ const SdkCustomConfigSchema: typeof TypescriptCustomConfigSchema = TypescriptCus
1111
type SdkCustomConfigSchema = TypescriptCustomConfigSchema;
1212

1313
export class ReadmeConfigBuilder {
14-
private endpointSnippets: FernGeneratorExec.Endpoint[];
14+
private readonly endpointSnippets: FernGeneratorExec.Endpoint[];
1515
private readonly fileResponseType: "stream" | "binary-response";
1616
private readonly fetchSupport: "node-fetch" | "native";
1717

generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -248,33 +248,53 @@ const response = await ${this.getMethodCall(queryStringEndpoint)}(..., {
248248
}
249249

250250
private buildFileUploadRequestSnippet(): string[] {
251-
const binaryRequestEndpoints = Object.values(this.context.ir.services).flatMap((service) =>
251+
const fileUploadEndpoints = Object.values(this.context.ir.services).flatMap((service) =>
252252
service.endpoints
253-
.filter((endpoint) => endpoint.requestBody?.type === "bytes")
253+
.filter((endpoint) => {
254+
if (endpoint.requestBody == null) {
255+
return false;
256+
}
257+
if (endpoint.requestBody.type === "bytes") {
258+
return true;
259+
}
260+
if (
261+
endpoint.requestBody.type === "fileUpload" &&
262+
endpoint.requestBody.properties.some((property) => property.type === "file")
263+
) {
264+
return true;
265+
}
266+
return false;
267+
})
254268
.map((endpoint) => ({
255269
endpoint,
256270
fernFilepath: service.name.fernFilepath
257271
}))
258272
);
259-
if (binaryRequestEndpoints.length === 0) {
260-
return [];
261-
}
262-
const binaryRequestEndpoint = binaryRequestEndpoints[0] as EndpointWithFilepath;
263-
return [
264-
this.writeCode(
265-
code`
273+
for (const fileUploadEndpoint of fileUploadEndpoints) {
274+
if (fileUploadEndpoint.endpoint.requestBody?.type === "bytes") {
275+
return [
276+
this.writeCode(
277+
code`
266278
import { createReadStream } from "fs";
267279
268-
await ${this.getMethodCall(binaryRequestEndpoint)}(createReadStream("path/to/file"), ...);
269-
await ${this.getMethodCall(binaryRequestEndpoint)}(new ReadableStream(), ...);
270-
await ${this.getMethodCall(binaryRequestEndpoint)}(Buffer.from('binary data'), ...);
271-
await ${this.getMethodCall(binaryRequestEndpoint)}(new Blob(['binary data'], { type: 'audio/mpeg' }), ...);
272-
await ${this.getMethodCall(binaryRequestEndpoint)}(new File(['binary data'], 'file.mp3'), ...);
273-
await ${this.getMethodCall(binaryRequestEndpoint)}(new ArrayBuffer(8), ...);
274-
await ${this.getMethodCall(binaryRequestEndpoint)}(new Uint8Array([0, 1, 2]), ...);
275-
`
276-
)
277-
];
280+
await ${this.getMethodCall(fileUploadEndpoint)}(createReadStream("path/to/file"), ...);
281+
await ${this.getMethodCall(fileUploadEndpoint)}(new ReadableStream(), ...);
282+
await ${this.getMethodCall(fileUploadEndpoint)}(Buffer.from('binary data'), ...);
283+
await ${this.getMethodCall(fileUploadEndpoint)}(new Blob(['binary data'], { type: 'audio/mpeg' }), ...);
284+
await ${this.getMethodCall(fileUploadEndpoint)}(new File(['binary data'], 'file.mp3'), ...);
285+
await ${this.getMethodCall(fileUploadEndpoint)}(new ArrayBuffer(8), ...);
286+
await ${this.getMethodCall(fileUploadEndpoint)}(new Uint8Array([0, 1, 2]), ...);
287+
`
288+
)
289+
];
290+
}
291+
292+
const snippet = this.getSnippetForEndpointId(fileUploadEndpoint.endpoint.id);
293+
if (snippet != null) {
294+
return [snippet];
295+
}
296+
}
297+
return [];
278298
}
279299

280300
private buildBinaryResponseSnippet(): string[] {

generators/typescript/sdk/versions.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
2+
- version: 3.10.0
3+
changelogEntry:
4+
- summary: |
5+
Generate file upload section in README.md for multipart-form file uploads.
6+
type: feat
7+
createdAt: "2025-10-20"
8+
irVersion: 60
9+
210
- version: 3.9.3
311
changelogEntry:
412
- summary: |

packages/cli/generation/ir-generator-tests/src/ir/__test__/test-definitions/file-upload.json

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2571,7 +2571,36 @@
25712571
},
25722572
"v2Responses": null,
25732573
"errors": [],
2574-
"userSpecifiedExamples": [],
2574+
"userSpecifiedExamples": [
2575+
{
2576+
"example": {
2577+
"id": "6d60be92",
2578+
"name": null,
2579+
"url": "/just-file",
2580+
"rootPathParameters": [],
2581+
"endpointPathParameters": [],
2582+
"servicePathParameters": [],
2583+
"endpointHeaders": [],
2584+
"serviceHeaders": [],
2585+
"queryParameters": [],
2586+
"request": {
2587+
"type": "inlinedRequestBody",
2588+
"properties": [],
2589+
"extraProperties": null,
2590+
"jsonExample": {}
2591+
},
2592+
"response": {
2593+
"type": "ok",
2594+
"value": {
2595+
"type": "body",
2596+
"value": null
2597+
}
2598+
},
2599+
"docs": null
2600+
},
2601+
"codeSamples": null
2602+
}
2603+
],
25752604
"autogeneratedExamples": [],
25762605
"pagination": null,
25772606
"transport": null,
@@ -5267,7 +5296,47 @@
52675296
},
52685297
"v2Responses": null,
52695298
"errors": [],
5270-
"userSpecifiedExamples": [],
5299+
"userSpecifiedExamples": [
5300+
{
5301+
"example": {
5302+
"id": "25dc561",
5303+
"name": null,
5304+
"url": "/optional-args",
5305+
"rootPathParameters": [],
5306+
"endpointPathParameters": [],
5307+
"servicePathParameters": [],
5308+
"endpointHeaders": [],
5309+
"serviceHeaders": [],
5310+
"queryParameters": [],
5311+
"request": {
5312+
"type": "inlinedRequestBody",
5313+
"properties": [],
5314+
"extraProperties": null,
5315+
"jsonExample": {}
5316+
},
5317+
"response": {
5318+
"type": "ok",
5319+
"value": {
5320+
"type": "body",
5321+
"value": {
5322+
"shape": {
5323+
"type": "primitive",
5324+
"primitive": {
5325+
"type": "string",
5326+
"string": {
5327+
"original": "Foo"
5328+
}
5329+
}
5330+
},
5331+
"jsonExample": "Foo"
5332+
}
5333+
}
5334+
},
5335+
"docs": null
5336+
},
5337+
"codeSamples": null
5338+
}
5339+
],
52715340
"autogeneratedExamples": [],
52725341
"pagination": null,
52735342
"transport": null,

packages/cli/register/src/ir-to-fdr-converter/__test__/__snapshots__/fdr/file-upload.json

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,25 @@
364364
"requestsV2": {},
365365
"responsesV2": {},
366366
"errorsV2": [],
367-
"examples": []
367+
"examples": [
368+
{
369+
"path": "/just-file",
370+
"pathParameters": {},
371+
"queryParameters": {},
372+
"headers": {},
373+
"requestBody": {},
374+
"requestBodyV3": {
375+
"type": "form",
376+
"value": {
377+
"file": {
378+
"type": "filename",
379+
"value": "<file1>"
380+
}
381+
}
382+
},
383+
"responseStatusCode": 204
384+
}
385+
]
368386
},
369387
{
370388
"auth": false,
@@ -890,7 +908,33 @@
890908
},
891909
"responsesV2": {},
892910
"errorsV2": [],
893-
"examples": []
911+
"examples": [
912+
{
913+
"path": "/optional-args",
914+
"pathParameters": {},
915+
"queryParameters": {},
916+
"headers": {},
917+
"requestBody": {},
918+
"requestBodyV3": {
919+
"type": "form",
920+
"value": {
921+
"image_file": {
922+
"type": "filename",
923+
"value": "<file1>"
924+
},
925+
"request": {
926+
"type": "json"
927+
}
928+
}
929+
},
930+
"responseStatusCode": 200,
931+
"responseBody": "Foo",
932+
"responseBodyV3": {
933+
"type": "json",
934+
"value": "Foo"
935+
}
936+
}
937+
]
894938
},
895939
{
896940
"auth": false,

seed/ts-sdk/file-upload/form-data-node16/README.md

Lines changed: 56 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)