diff --git a/packages/altair-core/src/request/__snapshots__/response-builder.spec.ts.snap b/packages/altair-core/src/request/__snapshots__/response-builder.spec.ts.snap index 794775aa6c..39eece3160 100644 --- a/packages/altair-core/src/request/__snapshots__/response-builder.spec.ts.snap +++ b/packages/altair-core/src/request/__snapshots__/response-builder.spec.ts.snap @@ -143,6 +143,24 @@ exports[`response-builder auto strategy should concatenate the responses when th ] `; +exports[`response-builder auto strategy should patch the responses when patchable - sample 2 1`] = ` +[ + { + "content": "{ + "data": { + "bookById": { + "name": "Effective Java", + "author": { + "firstName": "Joshua" + } + } + } +}", + "timestamp": 1718252802585, + }, +] +`; + exports[`response-builder auto strategy should patch the responses when the first response is patchable 1`] = ` [ { diff --git a/packages/altair-core/src/request/handlers/http.spec.ts b/packages/altair-core/src/request/handlers/http.spec.ts index 600ea20df0..7597520c98 100644 --- a/packages/altair-core/src/request/handlers/http.spec.ts +++ b/packages/altair-core/src/request/handlers/http.spec.ts @@ -652,6 +652,89 @@ describe('HTTP handler', () => { ]); }); + // https://github.com/felipe-gdr/spring-graphql-defer/issues/5 + it('should properly handle multipart streamed responses - sample 2', async () => { + const mockHandler = new MswMockRequestHandler( + 'http://localhost:3000/multipart-stream-2', + async () => { + const encoder = new TextEncoder(); + const stream = new ReadableStream({ + async start(controller) { + await delay(10); + // this is invalid since there is no boundary before the first content, so it should be ignored as the preamble as per the spec + controller.enqueue( + encoder.encode( + `\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{"data":{"bookById":{"name":"Effective Java"}},"hasNext":true}\r\n---` + ) + ); + await delay(10); + controller.enqueue( + encoder.encode( + `\r\ncontent-type: application/json; charset=utf-8\r\n\r\n{"hasNext":true,"incremental":[{"path":["bookById"],"data":{"author":{"firstName":"Joshua"}}}]}\r\n---` + ) + ); + await delay(10); + controller.enqueue( + encoder.encode( + `content-type: application/json; charset=utf-8\r\n\r\n{"hasNext":false,"incremental":[{"path":[],"data":{"book2":{"name":"Hitchhiker's Guide to the Galaxy"}}}]}\r\n-----` + ) + ); + await delay(10); + controller.close(); + }, + }); + + return new Response(stream, { + headers: { + 'content-type': 'multipart/mixed; boundary="-"', + }, + }); + } + ); + server.use(mockHandler); + const request: GraphQLRequestOptions = { + url: 'http://localhost:3000/multipart-stream-2', + method: 'POST', + additionalParams: { + testData: [ + { + hello: 'world', + }, + ], + }, + headers: [], + query: 'query { hello }', + variables: {}, + selectedOperation: 'hello', + }; + + const httpHandler: GraphQLRequestHandler = new HttpRequestHandler(); + const res = await testObserver(httpHandler.handle(request)); + + expect(res).toEqual([ + expect.objectContaining({ + ok: true, + data: '{"hasNext":true,"incremental":[{"path":["bookById"],"data":{"author":{"firstName":"Joshua"}}}]}', + headers: expect.any(Object), + status: 200, + url: 'http://localhost:3000/multipart-stream-2', + requestStartTimestamp: expect.any(Number), + requestEndTimestamp: expect.any(Number), + resopnseTimeMs: expect.any(Number), + }), + expect.objectContaining({ + ok: true, + data: `{"hasNext":false,"incremental":[{"path":[],"data":{"book2":{"name":"Hitchhiker's Guide to the Galaxy"}}}]}`, + headers: expect.any(Object), + status: 200, + url: 'http://localhost:3000/multipart-stream-2', + requestStartTimestamp: expect.any(Number), + requestEndTimestamp: expect.any(Number), + resopnseTimeMs: expect.any(Number), + }), + ]); + }); + it('should properly handle multipart streamed responses with errors', async () => { const mockHandler = new MswMockRequestHandler( 'http://localhost:3000/error-multipart-stream', diff --git a/packages/altair-core/src/request/handlers/http.ts b/packages/altair-core/src/request/handlers/http.ts index e2c64e52a7..7b4fa6bc7f 100644 --- a/packages/altair-core/src/request/handlers/http.ts +++ b/packages/altair-core/src/request/handlers/http.ts @@ -49,7 +49,7 @@ export class HttpRequestHandler implements GraphQLRequestHandler { if (!merosResponse.ok || !merosResponse.body) { // don't handle streaming - const buffer = await merosResponse.arrayBuffer() + const buffer = await merosResponse.arrayBuffer(); return this.emitChunk( merosResponse, new Uint8Array(buffer), diff --git a/packages/altair-core/src/request/response-builder.spec.ts b/packages/altair-core/src/request/response-builder.spec.ts index 7300bd5c9d..d1893cee43 100644 --- a/packages/altair-core/src/request/response-builder.spec.ts +++ b/packages/altair-core/src/request/response-builder.spec.ts @@ -165,13 +165,13 @@ describe('response-builder', () => { const res = buildResponse([ { content: '{"hello":', - timestamp: 1718252802585 + timestamp: 1718252802585, }, { content: '"world"}', timestamp: 1718252802585, - } - ]) + }, + ]); expect(res).toMatchSnapshot(); }); @@ -380,6 +380,26 @@ describe('response-builder', () => { expect(res).toMatchSnapshot(); }); + // https://github.com/felipe-gdr/spring-graphql-defer/issues/5 + it('should patch the responses when patchable - sample 2', () => { + const res = buildResponse([ + { + content: `{"data":{"bookById":{"name":"Effective Java"}},"hasNext":true}`, + timestamp: 1718252802585, + }, + { + content: `{"hasNext":true,"incremental":[{"path":["bookById"],"data":{"author":{"firstName":"Joshua"}}}]}`, + timestamp: 1718252802585, + }, + { + content: `{"hasNext":false,"incremental":[{"path":[],"data":{"book2":{"name":"Hitchhiker's Guide to the Galaxy"}}}]}`, + timestamp: 1718252802585, + }, + ]); + + expect(res).toMatchSnapshot(); + }); + it('should append the responses when the first response is a JSON object but not patchable', () => { const res = buildResponse( [