Skip to content

Commit eb45ad6

Browse files
committed
feat: support application/octet-stream req/res bodies
1 parent dc586d2 commit eb45ad6

15 files changed

Lines changed: 7496 additions & 13725 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"@biomejs/js-api": "2.0.3",
4444
"@biomejs/wasm-nodejs": "2.1.2",
4545
"@commander-js/extra-typings": "^14.0.0",
46+
"@jest/reporters": "^30.0.5",
4647
"@swc/core": "^1.13.2",
4748
"@swc/jest": "^0.2.39",
4849
"@tsconfig/node24": "^24.0.1",
@@ -78,5 +79,5 @@
7879
"engines": {
7980
"node": ">=20 <25"
8081
},
81-
"packageManager": "pnpm@10.13.1"
82+
"packageManager": "pnpm@10.14.0"
8283
}

packages/openapi-code-generator/src/typescript/client/typescript-axios/typescript-axios-client-builder.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
1616
"application/scim+json",
1717
"application/merge-patch+json",
1818
"application/x-www-form-urlencoded",
19+
"application/octet-stream",
1920
"text/json",
2021
"text/plain",
2122
"text/x-markdown",
@@ -67,6 +68,9 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
6768
const axiosFragment = `this._request({${[
6869
`url: url ${queryString ? "+ query" : ""}`,
6970
`method: "${method}"`,
71+
responseSchema?.type === "Blob"
72+
? "responseType: 'arraybuffer'"
73+
: undefined,
7074
requestBody?.parameter
7175
? requestBody.isSupported
7276
? "data: body"
@@ -102,7 +106,9 @@ export class TypescriptAxiosClientBuilder extends AbstractClientBuilder {
102106
103107
return {...res, data: ${this.schemaBuilder.parse(
104108
responseSchema.schema,
105-
"res.data",
109+
responseSchema.type === "Blob"
110+
? "new Blob([res.data], {type: res.headers['content-type'] || 'application/octet-stream'})"
111+
: "res.data",
106112
)}}
107113
`
108114
: `return ${axiosFragment}`
@@ -203,6 +209,16 @@ ${this.legacyExports(clientName)}
203209
return `${param} !== undefined ? ${serialize} : null`
204210
}
205211

212+
case "Blob": {
213+
const serialize = param
214+
215+
if (requestBody.parameter.required) {
216+
return serialize
217+
}
218+
219+
return `${param} !== undefined ? ${serialize} : null`
220+
}
221+
206222
default: {
207223
throw new Error(
208224
`typescript-axios does not support request bodies of content-type '${requestBody.contentType}' using serializer '${requestBody.serializer satisfies never}'`,

packages/openapi-code-generator/src/typescript/client/typescript-fetch/typescript-fetch-client-builder.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
1818
"application/scim+json",
1919
"application/merge-patch+json",
2020
"application/x-www-form-urlencoded",
21+
"application/octet-stream",
2122
"text/json",
2223
"text/plain",
2324
"text/x-markdown",
@@ -187,6 +188,16 @@ export class TypescriptFetchClientBuilder extends AbstractClientBuilder {
187188
return `${param} !== undefined ? ${serialize} : null`
188189
}
189190

191+
case "Blob": {
192+
const serialize = param
193+
194+
if (requestBody.parameter.required) {
195+
return serialize
196+
}
197+
198+
return `${param} !== undefined ? ${serialize} : null`
199+
}
200+
190201
default: {
191202
throw new Error(
192203
`typescript-fetch does not support request bodies of content-type '${requestBody.contentType}' using serializer '${requestBody.serializer satisfies never}'`,

packages/openapi-code-generator/src/typescript/common/schema-builders/abstract-schema-builder.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,15 @@ export abstract class AbstractSchemaBuilder<
223223
const model = this.input.schema(maybeModel)
224224

225225
switch (model.type) {
226-
case "string":
227-
result = this.string(model)
226+
case "string": {
227+
if (model.format === "byte" || model.format === "binary") {
228+
result = this.any()
229+
} else {
230+
result = this.string(model)
231+
}
232+
228233
break
234+
}
229235
case "number":
230236
result = this.number(model)
231237
break

packages/openapi-code-generator/src/typescript/common/type-builder.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,11 @@ export class TypeBuilder implements ICompilable {
174174
if (schemaObject["x-enum-extensibility"] === "open") {
175175
result.push(this.addStaticType("UnknownEnumStringValue"))
176176
}
177+
} else if (
178+
schemaObject.format === "binary" ||
179+
schemaObject.format === "byte"
180+
) {
181+
result.push("Blob")
177182
} else {
178183
result.push("string")
179184
}

packages/openapi-code-generator/src/typescript/common/typescript-common.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,12 @@ export function buildExport(args: ExportDefinition) {
141141
}
142142
}
143143

144-
export type Serializer = "JSON.stringify" | "String" | "URLSearchParams"
144+
export type Serializer =
145+
| "JSON.stringify"
146+
| "String"
147+
| "URLSearchParams"
148+
| "Blob"
145149
// TODO: support more serializations
146-
// | "Blob"
147150
// | "FormData"
148151

149152
export type RequestBodyAsParameter = {
@@ -169,10 +172,10 @@ function serializerForNormalizedContentType(contentType: string): Serializer {
169172
case "application/x-www-form-urlencoded":
170173
return "URLSearchParams"
171174

175+
case "application/octet-stream":
176+
return "Blob"
177+
172178
// TODO: support more serializations
173-
// case "application/octet-stream":
174-
// return "Blob"
175-
//
176179
// case "multipart/form-data":
177180
// return "FormData"
178181

packages/openapi-code-generator/src/typescript/server/abstract-router-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export abstract class AbstractRouterBuilder implements ICompilable {
1717
"application/scim+json",
1818
"application/merge-patch+json",
1919
"application/x-www-form-urlencoded",
20+
"application/octet-stream",
2021
"text/json",
2122
"text/plain",
2223
"text/x-markdown",

packages/openapi-code-generator/src/typescript/server/typescript-express/typescript-express-router-builder.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export class ExpressRouterBuilder extends AbstractRouterBuilder {
5252
"StatusCode3xx",
5353
"StatusCode4xx",
5454
"StatusCode5xx",
55+
"parseOctetStream",
56+
"sendResponse",
5557
)
5658

5759
this.imports
@@ -134,7 +136,7 @@ router.${builder.method.toLowerCase()}(\`${builder.route}\`, async (req: Request
134136
const input = {
135137
params: ${params.path.schema ? `parseRequestInput(${symbols.paramSchema}, req.params, RequestInputType.RouteParam)` : "undefined"},
136138
query: ${params.query.schema ? `parseRequestInput(${symbols.querySchema}, req.query, RequestInputType.QueryString)` : "undefined"},
137-
body: ${params.body.schema ? `parseRequestInput(${symbols.requestBodySchema}, req.body, RequestInputType.RequestBody)` : "undefined"},
139+
body: ${params.body.schema ? (params.body.contentType === "application/octet-stream" ? `parseRequestInput(${symbols.requestBodySchema}, await parseOctetStream(req), RequestInputType.RequestBody)` : `parseRequestInput(${symbols.requestBodySchema}, req.body, RequestInputType.RequestBody)`) : "undefined"},
138140
headers: ${params.header.schema ? `parseRequestInput(${symbols.requestHeaderSchema}, req.headers, RequestInputType.RequestHeader)` : "undefined"}
139141
}
140142
@@ -150,13 +152,7 @@ router.${builder.method.toLowerCase()}(\`${builder.route}\`, async (req: Request
150152
151153
const { status, body } = response instanceof ExpressRuntimeResponse ? response.unpack() : response
152154
153-
res.status(status)
154-
155-
if (body !== undefined) {
156-
res.json(${symbols.responseBodyValidator}(status, body))
157-
} else {
158-
res.end()
159-
}
155+
await sendResponse(res, status, body, ${symbols.responseBodyValidator})
160156
} catch (error) {
161157
next(error)
162158
}

packages/openapi-code-generator/src/typescript/server/typescript-koa/typescript-koa-router-builder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class KoaRouterBuilder extends AbstractRouterBuilder {
4949
"StatusCode4xx",
5050
"StatusCode5xx",
5151
"startServer",
52+
"parseOctetStream",
5253
)
5354

5455
this.imports
@@ -142,7 +143,7 @@ router.${builder.method.toLowerCase()}('${symbols.implPropName}','${builder.rout
142143
const input = {
143144
params: ${params.path.schema ? `parseRequestInput(${symbols.paramSchema}, ctx.params, RequestInputType.RouteParam)` : "undefined"},
144145
query: ${params.query.schema ? `parseRequestInput(${symbols.querySchema}, ctx.query, RequestInputType.QueryString)` : "undefined"},
145-
body: ${params.body.schema ? `parseRequestInput(${symbols.requestBodySchema}, Reflect.get(ctx.request, "body"), RequestInputType.RequestBody)` : "undefined"},
146+
body: ${params.body.schema ? (params.body.contentType === "application/octet-stream" ? `parseRequestInput(${symbols.requestBodySchema}, await parseOctetStream(ctx), RequestInputType.RequestBody)` : `parseRequestInput(${symbols.requestBodySchema}, Reflect.get(ctx.request, "body"), RequestInputType.RequestBody)`) : "undefined"},
146147
headers: ${params.header.schema ? `parseRequestInput(${symbols.requestHeaderSchema}, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` : "undefined"}
147148
}
148149

packages/typescript-express-runtime/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"test": "jest"
4646
},
4747
"dependencies": {
48+
"raw-body": "^3.0.0",
4849
"tslib": "^2.8.1"
4950
},
5051
"peerDependencies": {

0 commit comments

Comments
 (0)