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

Commit c84ac09

Browse files
committed
support for experimental defer-stream
1 parent ed00ba7 commit c84ac09

File tree

6 files changed

+426
-25
lines changed

6 files changed

+426
-25
lines changed

integrationTests/ts/package.json

Lines changed: 1 addition & 1 deletion
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": "15.4.0-experimental-stream-defer.1",
1010
"typescript-3.4": "npm:[email protected]",
1111
"typescript-3.5": "npm:[email protected]",
1212
"typescript-3.6": "npm:[email protected]",

package-lock.json

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

package.json

Lines changed: 1 addition & 1 deletion
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.6",
86-
"graphql": "15.4.0",
86+
"graphql": "15.4.0-experimental-stream-defer.1",
8787
"mocha": "8.2.1",
8888
"multer": "1.4.2",
8989
"nyc": "15.1.0",

src/__tests__/http-test.ts

Lines changed: 304 additions & 4 deletions
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 () => {
@@ -1260,6 +1371,108 @@ function runTests(server: Server) {
12601371
});
12611372
});
12621373

1374+
it('allows for custom error formatting in initial payload of async iterator', async () => {
1375+
const app = server();
1376+
1377+
app.post(
1378+
urlString(),
1379+
graphqlHTTP({
1380+
schema: TestSchema,
1381+
customFormatErrorFn(error) {
1382+
return { message: 'Custom error format: ' + error.message };
1383+
},
1384+
}),
1385+
);
1386+
1387+
const req = app
1388+
.request()
1389+
.post(urlString())
1390+
.send({
1391+
query:
1392+
'{ thrower, ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
1393+
})
1394+
.parse((res, cb) => {
1395+
res.on('data', (data) => {
1396+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1397+
});
1398+
res.on('end', (err) => {
1399+
cb(err, null);
1400+
});
1401+
});
1402+
1403+
const response = await req;
1404+
expect(response.text).to.equal(
1405+
[
1406+
'',
1407+
'---',
1408+
'Content-Type: application/json; charset=utf-8',
1409+
'Content-Length: 94',
1410+
'',
1411+
'{"errors":[{"message":"Custom error format: Throws!"}],"data":{"thrower":null},"hasNext":true}',
1412+
'',
1413+
'---',
1414+
'Content-Type: application/json; charset=utf-8',
1415+
'Content-Length: 57',
1416+
'',
1417+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
1418+
'',
1419+
'-----',
1420+
'',
1421+
].join('\r\n'),
1422+
);
1423+
});
1424+
1425+
it('allows for custom error formatting in subsequent payloads of async iterator', async () => {
1426+
const app = server();
1427+
1428+
app.post(
1429+
urlString(),
1430+
graphqlHTTP({
1431+
schema: TestSchema,
1432+
customFormatErrorFn(error) {
1433+
return { message: 'Custom error format: ' + error.message };
1434+
},
1435+
}),
1436+
);
1437+
1438+
const req = app
1439+
.request()
1440+
.post(urlString())
1441+
.send({
1442+
query:
1443+
'{ test(who: "World"), ...frag @defer } fragment frag on QueryRoot { thrower }',
1444+
})
1445+
.parse((res, cb) => {
1446+
res.on('data', (data) => {
1447+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
1448+
});
1449+
res.on('end', (err) => {
1450+
cb(err, null);
1451+
});
1452+
});
1453+
1454+
const response = await req;
1455+
expect(response.text).to.equal(
1456+
[
1457+
'',
1458+
'---',
1459+
'Content-Type: application/json; charset=utf-8',
1460+
'Content-Length: 46',
1461+
'',
1462+
'{"data":{"test":"Hello World"},"hasNext":true}',
1463+
'',
1464+
'---',
1465+
'Content-Type: application/json; charset=utf-8',
1466+
'Content-Length: 105',
1467+
'',
1468+
'{"data":{"thrower":null},"path":[],"errors":[{"message":"Custom error format: Throws!"}],"hasNext":false}',
1469+
'',
1470+
'-----',
1471+
'',
1472+
].join('\r\n'),
1473+
);
1474+
});
1475+
12631476
it('allows for custom error formatting to elaborate', async () => {
12641477
const app = server();
12651478

@@ -2100,6 +2313,10 @@ function runTests(server: Server) {
21002313
async customExecuteFn(args) {
21012314
seenExecuteArgs = args;
21022315
const result = await Promise.resolve(execute(args));
2316+
// istanbul ignore if this test query will never return an async iterable
2317+
if (isAsyncIterable(result)) {
2318+
return result;
2319+
}
21032320
return {
21042321
...result,
21052322
data: {
@@ -2253,6 +2470,57 @@ function runTests(server: Server) {
22532470
});
22542471
});
22552472

2473+
it('allows for custom extensions in initial and subsequent payloads of async iterator', async () => {
2474+
const app = server();
2475+
2476+
app.post(
2477+
urlString(),
2478+
graphqlHTTP({
2479+
schema: TestSchema,
2480+
extensions({ result }) {
2481+
return { preservedResult: { ...result } };
2482+
},
2483+
}),
2484+
);
2485+
2486+
const req = app
2487+
.request()
2488+
.post(urlString())
2489+
.send({
2490+
query:
2491+
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
2492+
})
2493+
.parse((res, cb) => {
2494+
res.on('data', (data) => {
2495+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2496+
});
2497+
res.on('end', (err) => {
2498+
cb(err, null);
2499+
});
2500+
});
2501+
2502+
const response = await req;
2503+
expect(response.text).to.equal(
2504+
[
2505+
'',
2506+
'---',
2507+
'Content-Type: application/json; charset=utf-8',
2508+
'Content-Length: 124',
2509+
'',
2510+
'{"data":{"hello":"Hello Rob"},"hasNext":true,"extensions":{"preservedResult":{"data":{"hello":"Hello Rob"},"hasNext":true}}}',
2511+
'',
2512+
'---',
2513+
'Content-Type: application/json; charset=utf-8',
2514+
'Content-Length: 148',
2515+
'',
2516+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false,"extensions":{"preservedResult":{"data":{"test":"Hello World"},"path":[],"hasNext":false}}}',
2517+
'',
2518+
'-----',
2519+
'',
2520+
].join('\r\n'),
2521+
);
2522+
});
2523+
22562524
it('extension function may be async', async () => {
22572525
const app = server();
22582526

@@ -2293,12 +2561,44 @@ function runTests(server: Server) {
22932561

22942562
const response = await app
22952563
.request()
2296-
.get(urlString({ query: '{test}', raw: '' }))
2297-
.set('Accept', 'text/html');
2564+
.get(
2565+
urlString({
2566+
query:
2567+
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
2568+
raw: '',
2569+
}),
2570+
)
2571+
.set('Accept', 'text/html')
2572+
.parse((res, cb) => {
2573+
res.on('data', (data) => {
2574+
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
2575+
});
2576+
res.on('end', (err) => {
2577+
cb(err, null);
2578+
});
2579+
});
22982580

22992581
expect(response.status).to.equal(200);
2300-
expect(response.type).to.equal('application/json');
2301-
expect(response.text).to.equal('{"data":{"test":"Hello World"}}');
2582+
expect(response.type).to.equal('multipart/mixed');
2583+
expect(response.text).to.equal(
2584+
[
2585+
'',
2586+
'---',
2587+
'Content-Type: application/json; charset=utf-8',
2588+
'Content-Length: 45',
2589+
'',
2590+
'{"data":{"hello":"Hello Rob"},"hasNext":true}',
2591+
'',
2592+
'---',
2593+
'Content-Type: application/json; charset=utf-8',
2594+
'Content-Length: 57',
2595+
'',
2596+
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
2597+
'',
2598+
'-----',
2599+
'',
2600+
].join('\r\n'),
2601+
);
23022602
});
23032603
});
23042604
}

0 commit comments

Comments
 (0)