Skip to content

Commit 0036330

Browse files
authored
Add support to raw body in request (#316)
* update ts-commons * add support to body as ReadableStream * add test to send raw body * fix lint
1 parent 8f1da73 commit 0036330

10 files changed

Lines changed: 174 additions & 29 deletions

File tree

e2e/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"dependencies": {
2323
"@pagopa/openapi-codegen-ts": "../",
24-
"@pagopa/ts-commons": "^10.8.0",
24+
"@pagopa/ts-commons": "^10.14.2",
2525
"fp-ts": "^2.10.5",
2626
"io-ts": "^2.2.16"
2727
},

e2e/src/__tests__/test-api-v3/client.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { createClient } from "../../generated/testapiV3/client";
77
// @ts-ignore because leaked-handles doesn't ship type defintions
88
import * as leaked from "leaked-handles";
99
import { NewModel } from "../../generated/testapiV3/NewModel";
10+
import { Readable } from "stream";
11+
import { closeServer, startServer } from "../../server";
12+
import { IncomingMessage, ServerResponse } from "http";
1013
leaked.set({ debugSockets: true });
1114

1215
// Use same config as
@@ -341,4 +344,40 @@ describeSuite("Http client generated from Test API spec", () => {
341344
// @ts-expect-error
342345
client.putTestParameterWithBodyReference({ body: "" });
343346
});
347+
348+
349+
it("should handle model ref model in body as readablestream", async () => {
350+
const aData: NewModel = {
351+
id: "anId",
352+
name: "aName"
353+
};
354+
355+
const mockTestEndpoint = jest.fn((request: IncomingMessage, response: ServerResponse) => {
356+
let data = "";
357+
request.on("data", chunk => data += chunk)
358+
request.on("end", () => {
359+
expect(JSON.parse(data)).toEqual(aData);
360+
response.statusCode = 201;
361+
response.end();
362+
})
363+
364+
} );
365+
const server = await startServer(mockPort+10, mockTestEndpoint);
366+
367+
const client = createClient({
368+
baseUrl: `http://localhost:${mockPort+10}`,
369+
fetchApi: (nodeFetch as any) as typeof fetch,
370+
basePath: ""
371+
});
372+
373+
const aDataAsBuffer = Readable.from(Buffer.from(JSON.stringify(aData))) as unknown as ReadableStream<Uint8Array>;
374+
375+
expect(client.testParameterWithBodyReference).toEqual(expect.any(Function));
376+
const response = await client.testParameterWithBodyReference({ body: aDataAsBuffer });
377+
378+
expect(mockTestEndpoint).toHaveBeenCalledTimes(1);
379+
380+
381+
await closeServer(server);
382+
});
344383
});

e2e/src/__tests__/test-api/client.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import { createClient } from "../../generated/testapi/client";
77
// @ts-ignore because leaked-handles doesn't ship type defintions
88
import * as leaked from "leaked-handles";
99
import { NewModel } from "../../generated/testapi/NewModel";
10+
import { Readable } from "stream";
11+
import { IncomingMessage, ServerResponse } from "http";
12+
import { startServer, closeServer } from "../../server";
1013
leaked.set({ debugSockets: true });
1114

1215
const { skipClient } = config;
@@ -340,4 +343,39 @@ describeSuite("Http client generated from Test API spec", () => {
340343
// @ts-expect-error
341344
client.putTestParameterWithBodyReference({ body: "" });
342345
});
346+
347+
it("should handle model ref model in body as readablestream", async () => {
348+
const aData: NewModel = {
349+
id: "anId",
350+
name: "aName"
351+
};
352+
353+
const mockTestEndpoint = jest.fn((request: IncomingMessage, response: ServerResponse) => {
354+
let data = "";
355+
request.on("data", chunk => data += chunk)
356+
request.on("end", () => {
357+
expect(JSON.parse(data)).toEqual(aData);
358+
response.statusCode = 201;
359+
response.end();
360+
})
361+
362+
} );
363+
const server = await startServer(mockPort+10, mockTestEndpoint);
364+
365+
const client = createClient({
366+
baseUrl: `http://localhost:${mockPort+10}`,
367+
fetchApi: (nodeFetch as any) as typeof fetch,
368+
basePath: ""
369+
});
370+
371+
const aDataAsBuffer = Readable.from(Buffer.from(JSON.stringify(aData))) as unknown as ReadableStream<Uint8Array>;
372+
373+
expect(client.testParameterWithBodyReference).toEqual(expect.any(Function));
374+
const response = await client.testParameterWithBodyReference({ body: aDataAsBuffer });
375+
376+
expect(mockTestEndpoint).toHaveBeenCalledTimes(1);
377+
378+
379+
await closeServer(server);
380+
});
343381
});

e2e/src/server.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,35 @@
1+
/* eslint-disable functional/immutable-data */
12
/**
23
* This module abstracts the start and stop of a Prism mock server (https://github.com/stoplightio/prism).
34
*
45
*/
56

7+
import { once } from "events";
8+
import { createServer as createServerWithHttp, Server } from "http";
69
import { createLogger } from "@stoplight/prism-core";
710
import { getHttpOperationsFromResource } from "@stoplight/prism-http";
811
import { createServer } from "@stoplight/prism-http-server";
912

1013
const servers = new Map<number, ReturnType<typeof createServer>>();
1114

15+
const startServer = async (
16+
port: number,
17+
mockGetUserSession: jest.Mock
18+
): Promise<Server> => {
19+
const server = createServerWithHttp((request, response) => {
20+
if (request.url?.startsWith("/test-parameter-with-body-ref")) {
21+
mockGetUserSession(request, response);
22+
} else {
23+
response.statusCode = 500;
24+
response.end();
25+
}
26+
}).listen(port);
27+
28+
await once(server, "listening");
29+
30+
return server;
31+
};
32+
1233
/**
1334
* Starts a mock server for a given specification
1435
*
@@ -19,7 +40,6 @@ const servers = new Map<number, ReturnType<typeof createServer>>();
1940
*/
2041
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, prefer-arrow/prefer-arrow-functions
2142
function startMockServer(apiSpecUrl: string, port: number = 4100) {
22-
const startedAt = Date.now();
2343
return getHttpOperationsFromResource(apiSpecUrl)
2444
.then(operations =>
2545
createServer(operations, {
@@ -37,15 +57,14 @@ function startMockServer(apiSpecUrl: string, port: number = 4100) {
3757
)
3858
.then(async server => {
3959
await server.listen(port);
40-
// eslint-disable-next-line no-console
41-
console.log(
42-
`server started on port ${port} after ${Date.now() - startedAt}ms`
43-
);
4460
return server;
4561
})
4662
.then(server => servers.set(port, server));
4763
}
4864

65+
const closeServer = (server: Server): Promise<void> =>
66+
new Promise(done => server.close(done)).then(_ => void 0);
67+
4968
/**
5069
* Stop all the servers previously started
5170
*
@@ -64,4 +83,4 @@ function stopAllServers() {
6483
).then(() => servers.clear());
6584
}
6685

67-
export { startMockServer, stopAllServers };
86+
export { startMockServer, stopAllServers, closeServer, startServer };

e2e/yarn.lock

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@
481481
deep-equal "^1.0.1"
482482

483483
"@pagopa/openapi-codegen-ts@../":
484-
version "12.0.2"
484+
version "12.1.3"
485485
dependencies:
486486
"@pagopa/ts-commons" "^10.3.0"
487487
fs-extra "^6.0.0"
@@ -493,24 +493,26 @@
493493
write-yaml-file "^4.1.3"
494494
yargs "^15.0.1"
495495

496-
"@pagopa/ts-commons@^10.3.0":
497-
version "10.4.0"
498-
resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.4.0.tgz#a422295e24735cfba26b744b055f90f718fd9aef"
499-
integrity sha512-4/qbF+WjyjYJqMG8otj3qBwGe4g4DqZ8haou1GkuC4cYog6Yv+BfQT7TbV+dNsTGYbuc6QRw0eHXBPvbjpD7dw==
496+
"@pagopa/ts-commons@^10.14.2":
497+
version "10.14.2"
498+
resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.14.2.tgz#a27bdbe00a376d0f71e9d2859ebaedc1d16b6a10"
499+
integrity sha512-rRSAbAnIZidEsM8Vto1TK0jOKOOs8MUFora72OSM0VM18RWp1gsVd3WC7JbPaw8vztX+wB7PINBJUdq66j+aow==
500500
dependencies:
501501
abort-controller "^3.0.0"
502502
agentkeepalive "^4.1.4"
503503
applicationinsights "^1.8.10"
504504
fp-ts "^2.11.0"
505505
io-ts "^2.2.16"
506+
jose "^4.11.2"
506507
json-set-map "^1.1.2"
507508
node-fetch "^2.6.0"
509+
semver "^7.3.7"
508510
validator "^13.7.0"
509511

510-
"@pagopa/ts-commons@^10.8.0":
511-
version "10.8.0"
512-
resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.8.0.tgz#2ee54077cf09edf254442e1aad67427078a69edd"
513-
integrity sha512-LAdzrwnHeAX5+NuTtd+H9Dwz6Z+MqRVcofKHHlF37O6YoYmNzWmT4+LInjAGIZWnjoEtsBM+8W9PsS6ndX2f6w==
512+
"@pagopa/ts-commons@^10.3.0":
513+
version "10.4.0"
514+
resolved "https://registry.yarnpkg.com/@pagopa/ts-commons/-/ts-commons-10.4.0.tgz#a422295e24735cfba26b744b055f90f718fd9aef"
515+
integrity sha512-4/qbF+WjyjYJqMG8otj3qBwGe4g4DqZ8haou1GkuC4cYog6Yv+BfQT7TbV+dNsTGYbuc6QRw0eHXBPvbjpD7dw==
514516
dependencies:
515517
abort-controller "^3.0.0"
516518
agentkeepalive "^4.1.4"
@@ -2826,6 +2828,11 @@ jest@^25.2.7:
28262828
import-local "^3.0.2"
28272829
jest-cli "^25.3.0"
28282830

2831+
jose@^4.11.2:
2832+
version "4.13.1"
2833+
resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c"
2834+
integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==
2835+
28292836
js-tokens@^4.0.0:
28302837
version "4.0.0"
28312838
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -3127,6 +3134,13 @@ lolex@^5.0.0:
31273134
dependencies:
31283135
"@sinonjs/commons" "^1.7.0"
31293136

3137+
lru-cache@^6.0.0:
3138+
version "6.0.0"
3139+
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
3140+
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
3141+
dependencies:
3142+
yallist "^4.0.0"
3143+
31303144
make-dir@^3.0.0:
31313145
version "3.0.2"
31323146
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392"
@@ -3980,6 +3994,13 @@ semver@^5.3.0, semver@^5.4.1, semver@^5.5.0:
39803994
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
39813995
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
39823996

3997+
semver@^7.3.7:
3998+
version "7.3.8"
3999+
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798"
4000+
integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==
4001+
dependencies:
4002+
lru-cache "^6.0.0"
4003+
39834004
set-blocking@^2.0.0:
39844005
version "2.0.0"
39854006
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@@ -4722,6 +4743,11 @@ y18n@^4.0.0, y18n@^4.0.1:
47224743
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
47234744
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
47244745

4746+
yallist@^4.0.0:
4747+
version "4.0.0"
4748+
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
4749+
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
4750+
47254751
yargs-parser@18.x, yargs-parser@^18.1.1:
47264752
version "18.1.2"
47274753
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.2.tgz#2f482bea2136dbde0861683abea7756d30b504f1"

src/commands/gen-api-models/__tests__/__snapshots__/index.test.ts.snap

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,7 +1441,11 @@ export function createClient<K extends ParamKeys>({
14411441
response_decoder: testParameterWithBodyReferenceDefaultDecoder(),
14421442
url: ({}) => \`\${basePath}/test-parameter-with-body-ref\`,
14431443

1444-
body: ({ [\\"body\\"]: body }) => JSON.stringify(body),
1444+
body: ({ [\\"body\\"]: body }) =>
1445+
body?.constructor?.name === \\"Readable\\" ||
1446+
body?.constructor?.name === \\"ReadableStream\\"
1447+
? (body as ReadableStream)
1448+
: JSON.stringify(body),
14451449

14461450
query: () => withoutUndefinedValues({})
14471451
};
@@ -1462,7 +1466,11 @@ export function createClient<K extends ParamKeys>({
14621466
response_decoder: putTestParameterWithBodyReferenceDefaultDecoder(),
14631467
url: ({}) => \`\${basePath}/put-test-parameter-with-body-ref\`,
14641468

1465-
body: ({ [\\"body\\"]: body }) => JSON.stringify(body),
1469+
body: ({ [\\"body\\"]: body }) =>
1470+
body?.constructor?.name === \\"Readable\\" ||
1471+
body?.constructor?.name === \\"ReadableStream\\"
1472+
? (body as ReadableStream)
1473+
: JSON.stringify(body),
14661474

14671475
query: () => withoutUndefinedValues({})
14681476
};
@@ -3309,7 +3317,11 @@ export function createClient<K extends ParamKeys>({
33093317
response_decoder: testParameterWithBodyReferenceDefaultDecoder(),
33103318
url: ({}) => \`\${basePath}/test-parameter-with-body-ref\`,
33113319

3312-
body: ({ [\\"body\\"]: body }) => JSON.stringify(body),
3320+
body: ({ [\\"body\\"]: body }) =>
3321+
body?.constructor?.name === \\"Readable\\" ||
3322+
body?.constructor?.name === \\"ReadableStream\\"
3323+
? (body as ReadableStream)
3324+
: JSON.stringify(body),
33133325

33143326
query: () => withoutUndefinedValues({})
33153327
};
@@ -3330,7 +3342,11 @@ export function createClient<K extends ParamKeys>({
33303342
response_decoder: putTestParameterWithBodyReferenceDefaultDecoder(),
33313343
url: ({}) => \`\${basePath}/put-test-parameter-with-body-ref\`,
33323344

3333-
body: ({ [\\"body\\"]: body }) => JSON.stringify(body),
3345+
body: ({ [\\"body\\"]: body }) =>
3346+
body?.constructor?.name === \\"Readable\\" ||
3347+
body?.constructor?.name === \\"ReadableStream\\"
3348+
? (body as ReadableStream)
3349+
: JSON.stringify(body),
33343350

33353351
query: () => withoutUndefinedValues({})
33363352
};

src/commands/gen-api-models/__tests__/parse.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ describe.each`
208208
{
209209
name: "body?",
210210
in: "body",
211-
type: "NewModel"
211+
type: "NewModel | ReadableStream<Uint8Array>"
212212
}
213213
])
214214
})
@@ -233,7 +233,7 @@ describe.each`
233233
{
234234
name: "body?",
235235
in: "body",
236-
type: "NewModel"
236+
type: "NewModel | ReadableStream<Uint8Array>"
237237
}
238238
])
239239
})

src/commands/gen-api-models/parse.v2.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,11 @@ export const parseOperation = (
291291
// eslint-disable-next-line @typescript-eslint/no-use-before-define
292292
.map(parseParameter(specParameters, operationId))
293293
.filter((e): e is IParameterInfo => typeof e !== "undefined")
294+
.map(param =>
295+
param.in === "body"
296+
? { ...param, type: `${param.type} | ReadableStream<Uint8Array>` }
297+
: param
298+
)
294299
: [];
295300

296301
const authHeadersAndParams = operation.security

src/commands/gen-api-models/parse.v3.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,6 @@ export const parseOperation = (
334334
operation.requestBody.required ?? false
335335
]
336336
: [undefined, false];
337-
338337
const bodyParam: ReadonlyArray<IParameterInfo> =
339338
["post", "put"].includes(method) && bodySchema
340339
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -352,11 +351,10 @@ export const parseOperation = (
352351
{
353352
in: "body",
354353
name: `body${bodyRequired ? "" : "?"}`,
355-
type:
356-
bodySchema.$ref
357-
?.split("/")
358-
.slice()
359-
.pop() ?? ""
354+
type: `${bodySchema.$ref
355+
?.split("/")
356+
.slice()
357+
.pop() ?? ""} | ReadableStream<Uint8Array>`
360358
}
361359
] as ReadonlyArray<IParameterInfo>)
362360
: [

templates/client.ts.njk

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ export function createClient<K extends ParamKeys>({
148148
response_decoder: {{ macro.responseDecoderName(operation) }}(),
149149
url: ({ {{ pathParams | pick("name") | stripQuestionMark | safeDestruct | join(', ') | safe }} }) => `{{ macro.$("basePath") }}{{ macro.applyPathParams(operation.path, pathParams | pick("name") | stripQuestionMark) }}`,
150150
{% if bodyParams | length %}
151-
body: ({ {{ bodyParams | pick("name") | stripQuestionMark | safeDestruct | join(", ") | safe }} }) => JSON.stringify({{ bodyParams | pick("name") | stripQuestionMark | safeIdentifier | join(", ") | safe }}),
151+
{% set bodyParamName = bodyParams | pick("name") | stripQuestionMark | safeIdentifier | join(", ") | safe %}
152+
body: ({ {{ bodyParams | pick("name") | stripQuestionMark | safeDestruct | join(", ") | safe }} }) =>
153+
{{ bodyParamName }}?.constructor?.name === "Readable" || {{ bodyParamName }}?.constructor?.name === "ReadableStream"
154+
? ({{ bodyParamName }} as ReadableStream)
155+
:JSON.stringify({{ bodyParamName }}),
152156
{% elif formParams | length %}
153157
body: ({ {{ formParams | pick("name") | stripQuestionMark | safeDestruct | join(", ") | safe }} }) => {{ formParams | pick("name") | safeIdentifier | first }}.uri,
154158
{% elif formParamsBinary | length %}

0 commit comments

Comments
 (0)