Skip to content
This repository was archived by the owner on Mar 20, 2023. It is now read-only.

Commit 3505ce2

Browse files
committed
support for experimental defer-stream
flush response if compression middleware is used
1 parent 0fe6510 commit 3505ce2

File tree

6 files changed

+425
-25
lines changed

6 files changed

+425
-25
lines changed

integrationTests/ts/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dependencies": {
77
"@types/node": "14.0.13",
88
"express-graphql": "file:../express-graphql.tgz",
9-
"graphql": "14.7.0",
9+
"graphql": "https://registry.npmjs.org/graphql-experimental/-/graphql-experimental-5.0.2.tgz",
1010
"typescript-3.4": "npm:typescript@3.4.x",
1111
"typescript-3.5": "npm:typescript@3.5.x",
1212
"typescript-3.6": "npm:typescript@3.6.x",

package-lock.json

+2-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383
"eslint-plugin-node": "11.1.0",
8484
"express": "4.17.1",
8585
"graphiql": "1.0.3",
86-
"graphql": "15.3.0",
86+
"graphql": "https://registry.npmjs.org/graphql-experimental/-/graphql-experimental-5.0.2.tgz",
8787
"mocha": "8.1.3",
8888
"multer": "1.4.2",
8989
"nyc": "15.1.0",

src/__tests__/http-test.ts

+304-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from 'graphql';
2626

2727
import { graphqlHTTP } from '../index';
28+
import { isAsyncIterable } from '../isAsyncIterable';
2829

2930
type Middleware = (req: any, res: any, next: () => void) => unknown;
3031
type Server = () => {
@@ -1027,6 +1028,60 @@ function runTests(server: Server) {
10271028
errors: [{ message: 'Must provide query string.' }],
10281029
});
10291030
});
1031+
1032+
it('allows for streaming results with @defer', async () => {
1033+
const app = server();
1034+
const fakeFlush = sinon.fake();
1035+
1036+
app.use((_, res, next) => {
1037+
res.flush = fakeFlush;
1038+
next();
1039+
});
1040+
app.post(
1041+
urlString(),
1042+
graphqlHTTP({
1043+
schema: TestSchema,
1044+
}),
1045+
);
1046+
1047+
const req = app
1048+
.request()
1049+
.post(urlString())
1050+
.send({
1051+
query:
1052+
'{ ...frag @defer(label: "deferLabel") } fragment frag on QueryRoot { test(who: "World") }',
1053+
})
1054+
.parse((res, cb) => {
1055+
res.on('data', (data) => {
1056+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1057+
});
1058+
res.on('end', (err) => {
1059+
cb(err, null);
1060+
});
1061+
});
1062+
1063+
const response = await req;
1064+
expect(fakeFlush.callCount).to.equal(2);
1065+
expect(response.text).to.equal(
1066+
[
1067+
'',
1068+
'---',
1069+
'Content-Type: application/json; charset=utf-8',
1070+
'Content-Length: 26',
1071+
'',
1072+
'{"data":{},"hasNext":true}',
1073+
'',
1074+
'---',
1075+
'Content-Type: application/json; charset=utf-8',
1076+
'Content-Length: 78',
1077+
'',
1078+
'{"data":{"test":"Hello World"},"path":[],"label":"deferLabel","hasNext":false}',
1079+
'',
1080+
'-----',
1081+
'',
1082+
].join('\r\n'),
1083+
);
1084+
});
10301085
});
10311086

10321087
describe('Pretty printing', () => {
@@ -1109,6 +1164,62 @@ function runTests(server: Server) {
11091164

11101165
expect(unprettyResponse.text).to.equal('{"data":{"test":"Hello World"}}');
11111166
});
1167+
it('supports pretty printing async iterable requests', async () => {
1168+
const app = server();
1169+
1170+
app.post(
1171+
urlString(),
1172+
graphqlHTTP({
1173+
schema: TestSchema,
1174+
pretty: true,
1175+
}),
1176+
);
1177+
1178+
const req = app
1179+
.request()
1180+
.post(urlString())
1181+
.send({
1182+
query:
1183+
'{ ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
1184+
})
1185+
.parse((res, cb) => {
1186+
res.on('data', (data) => {
1187+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1188+
});
1189+
res.on('end', (err) => {
1190+
cb(err, null);
1191+
});
1192+
});
1193+
1194+
const response = await req;
1195+
expect(response.text).to.equal(
1196+
[
1197+
'',
1198+
'---',
1199+
'Content-Type: application/json; charset=utf-8',
1200+
'Content-Length: 35',
1201+
'',
1202+
['{', ' "data": {},', ' "hasNext": true', '}'].join('\n'),
1203+
'',
1204+
'---',
1205+
'Content-Type: application/json; charset=utf-8',
1206+
'Content-Length: 79',
1207+
'',
1208+
[
1209+
'{',
1210+
' "data": {',
1211+
' "test": "Hello World"',
1212+
' },',
1213+
' "path": [],',
1214+
' "hasNext": false',
1215+
'}',
1216+
].join('\n'),
1217+
'',
1218+
'-----',
1219+
'',
1220+
].join('\r\n'),
1221+
);
1222+
});
11121223
});
11131224

11141225
it('will send request and response when using thunk', async () => {
@@ -1229,6 +1340,108 @@ function runTests(server: Server) {
12291340
});
12301341
});
12311342

1343+
it('allows for custom error formatting in initial payload of async iterator', async () => {
1344+
const app = server();
1345+
1346+
app.post(
1347+
urlString(),
1348+
graphqlHTTP({
1349+
schema: TestSchema,
1350+
customFormatErrorFn(error) {
1351+
return { message: 'Custom error format: ' + error.message };
1352+
},
1353+
}),
1354+
);
1355+
1356+
const req = app
1357+
.request()
1358+
.post(urlString())
1359+
.send({
1360+
query:
1361+
'{ thrower, ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
1362+
})
1363+
.parse((res, cb) => {
1364+
res.on('data', (data) => {
1365+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1366+
});
1367+
res.on('end', (err) => {
1368+
cb(err, null);
1369+
});
1370+
});
1371+
1372+
const response = await req;
1373+
expect(response.text).to.equal(
1374+
[
1375+
'',
1376+
'---',
1377+
'Content-Type: application/json; charset=utf-8',
1378+
'Content-Length: 94',
1379+
'',
1380+
'{"errors":[{"message":"Custom error format: Throws!"}],"data":{"thrower":null},"hasNext":true}',
1381+
'',
1382+
'---',
1383+
'Content-Type: application/json; charset=utf-8',
1384+
'Content-Length: 57',
1385+
'',
1386+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
1387+
'',
1388+
'-----',
1389+
'',
1390+
].join('\r\n'),
1391+
);
1392+
});
1393+
1394+
it('allows for custom error formatting in subsequent payloads of async iterator', async () => {
1395+
const app = server();
1396+
1397+
app.post(
1398+
urlString(),
1399+
graphqlHTTP({
1400+
schema: TestSchema,
1401+
customFormatErrorFn(error) {
1402+
return { message: 'Custom error format: ' + error.message };
1403+
},
1404+
}),
1405+
);
1406+
1407+
const req = app
1408+
.request()
1409+
.post(urlString())
1410+
.send({
1411+
query:
1412+
'{ test(who: "World"), ...frag @defer } fragment frag on QueryRoot { thrower }',
1413+
})
1414+
.parse((res, cb) => {
1415+
res.on('data', (data) => {
1416+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1417+
});
1418+
res.on('end', (err) => {
1419+
cb(err, null);
1420+
});
1421+
});
1422+
1423+
const response = await req;
1424+
expect(response.text).to.equal(
1425+
[
1426+
'',
1427+
'---',
1428+
'Content-Type: application/json; charset=utf-8',
1429+
'Content-Length: 46',
1430+
'',
1431+
'{"data":{"test":"Hello World"},"hasNext":true}',
1432+
'',
1433+
'---',
1434+
'Content-Type: application/json; charset=utf-8',
1435+
'Content-Length: 105',
1436+
'',
1437+
'{"data":{"thrower":null},"path":[],"errors":[{"message":"Custom error format: Throws!"}],"hasNext":false}',
1438+
'',
1439+
'-----',
1440+
'',
1441+
].join('\r\n'),
1442+
);
1443+
});
1444+
12321445
it('allows for custom error formatting to elaborate', async () => {
12331446
const app = server();
12341447

@@ -2069,6 +2282,10 @@ function runTests(server: Server) {
20692282
async customExecuteFn(args) {
20702283
seenExecuteArgs = args;
20712284
const result = await Promise.resolve(execute(args));
2285+
// istanbul ignore if this test query will never return an async iterable
2286+
if (isAsyncIterable(result)) {
2287+
return result;
2288+
}
20722289
return {
20732290
...result,
20742291
data: {
@@ -2222,6 +2439,57 @@ function runTests(server: Server) {
22222439
});
22232440
});
22242441

2442+
it('allows for custom extensions in initial and subsequent payloads of async iterator', async () => {
2443+
const app = server();
2444+
2445+
app.post(
2446+
urlString(),
2447+
graphqlHTTP({
2448+
schema: TestSchema,
2449+
extensions({ result }) {
2450+
return { preservedResult: { ...result } };
2451+
},
2452+
}),
2453+
);
2454+
2455+
const req = app
2456+
.request()
2457+
.post(urlString())
2458+
.send({
2459+
query:
2460+
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
2461+
})
2462+
.parse((res, cb) => {
2463+
res.on('data', (data) => {
2464+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2465+
});
2466+
res.on('end', (err) => {
2467+
cb(err, null);
2468+
});
2469+
});
2470+
2471+
const response = await req;
2472+
expect(response.text).to.equal(
2473+
[
2474+
'',
2475+
'---',
2476+
'Content-Type: application/json; charset=utf-8',
2477+
'Content-Length: 124',
2478+
'',
2479+
'{"data":{"hello":"Hello Rob"},"hasNext":true,"extensions":{"preservedResult":{"data":{"hello":"Hello Rob"},"hasNext":true}}}',
2480+
'',
2481+
'---',
2482+
'Content-Type: application/json; charset=utf-8',
2483+
'Content-Length: 148',
2484+
'',
2485+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false,"extensions":{"preservedResult":{"data":{"test":"Hello World"},"path":[],"hasNext":false}}}',
2486+
'',
2487+
'-----',
2488+
'',
2489+
].join('\r\n'),
2490+
);
2491+
});
2492+
22252493
it('extension function may be async', async () => {
22262494
const app = server();
22272495

@@ -2262,12 +2530,44 @@ function runTests(server: Server) {
22622530

22632531
const response = await app
22642532
.request()
2265-
.get(urlString({ query: '{test}', raw: '' }))
2266-
.set('Accept', 'text/html');
2533+
.get(
2534+
urlString({
2535+
query:
2536+
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
2537+
raw: '',
2538+
}),
2539+
)
2540+
.set('Accept', 'text/html')
2541+
.parse((res, cb) => {
2542+
res.on('data', (data) => {
2543+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2544+
});
2545+
res.on('end', (err) => {
2546+
cb(err, null);
2547+
});
2548+
});
22672549

22682550
expect(response.status).to.equal(200);
2269-
expect(response.type).to.equal('application/json');
2270-
expect(response.text).to.equal('{"data":{"test":"Hello World"}}');
2551+
expect(response.type).to.equal('multipart/mixed');
2552+
expect(response.text).to.equal(
2553+
[
2554+
'',
2555+
'---',
2556+
'Content-Type: application/json; charset=utf-8',
2557+
'Content-Length: 45',
2558+
'',
2559+
'{"data":{"hello":"Hello Rob"},"hasNext":true}',
2560+
'',
2561+
'---',
2562+
'Content-Type: application/json; charset=utf-8',
2563+
'Content-Length: 57',
2564+
'',
2565+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
2566+
'',
2567+
'-----',
2568+
'',
2569+
].join('\r\n'),
2570+
);
22712571
});
22722572
});
22732573
}

0 commit comments

Comments
 (0)