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

Commit

Permalink
support for experimental defer-stream
Browse files Browse the repository at this point in the history
flush response if compression middleware is used
  • Loading branch information
robrichard committed Oct 15, 2020
1 parent 0fe6510 commit 3505ce2
Show file tree
Hide file tree
Showing 6 changed files with 425 additions and 25 deletions.
2 changes: 1 addition & 1 deletion integrationTests/ts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"dependencies": {
"@types/node": "14.0.13",
"express-graphql": "file:../express-graphql.tgz",
"graphql": "14.7.0",
"graphql": "https://registry.npmjs.org/graphql-experimental/-/graphql-experimental-5.0.2.tgz",
"typescript-3.4": "npm:[email protected]",
"typescript-3.5": "npm:[email protected]",
"typescript-3.6": "npm:[email protected]",
Expand Down
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"eslint-plugin-node": "11.1.0",
"express": "4.17.1",
"graphiql": "1.0.3",
"graphql": "15.3.0",
"graphql": "https://registry.npmjs.org/graphql-experimental/-/graphql-experimental-5.0.2.tgz",
"mocha": "8.1.3",
"multer": "1.4.2",
"nyc": "15.1.0",
Expand Down
308 changes: 304 additions & 4 deletions src/__tests__/http-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from 'graphql';

import { graphqlHTTP } from '../index';
import { isAsyncIterable } from '../isAsyncIterable';

type Middleware = (req: any, res: any, next: () => void) => unknown;
type Server = () => {
Expand Down Expand Up @@ -1027,6 +1028,60 @@ function runTests(server: Server) {
errors: [{ message: 'Must provide query string.' }],
});
});

it('allows for streaming results with @defer', async () => {
const app = server();
const fakeFlush = sinon.fake();

app.use((_, res, next) => {
res.flush = fakeFlush;
next();
});
app.post(
urlString(),
graphqlHTTP({
schema: TestSchema,
}),
);

const req = app
.request()
.post(urlString())
.send({
query:
'{ ...frag @defer(label: "deferLabel") } fragment frag on QueryRoot { test(who: "World") }',
})
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

const response = await req;
expect(fakeFlush.callCount).to.equal(2);
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 26',
'',
'{"data":{},"hasNext":true}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 78',
'',
'{"data":{"test":"Hello World"},"path":[],"label":"deferLabel","hasNext":false}',
'',
'-----',
'',
].join('\r\n'),
);
});
});

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

expect(unprettyResponse.text).to.equal('{"data":{"test":"Hello World"}}');
});
it('supports pretty printing async iterable requests', async () => {
const app = server();

app.post(
urlString(),
graphqlHTTP({
schema: TestSchema,
pretty: true,
}),
);

const req = app
.request()
.post(urlString())
.send({
query:
'{ ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
})
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

const response = await req;
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 35',
'',
['{', ' "data": {},', ' "hasNext": true', '}'].join('\n'),
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 79',
'',
[
'{',
' "data": {',
' "test": "Hello World"',
' },',
' "path": [],',
' "hasNext": false',
'}',
].join('\n'),
'',
'-----',
'',
].join('\r\n'),
);
});
});

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

it('allows for custom error formatting in initial payload of async iterator', async () => {
const app = server();

app.post(
urlString(),
graphqlHTTP({
schema: TestSchema,
customFormatErrorFn(error) {
return { message: 'Custom error format: ' + error.message };
},
}),
);

const req = app
.request()
.post(urlString())
.send({
query:
'{ thrower, ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
})
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

const response = await req;
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 94',
'',
'{"errors":[{"message":"Custom error format: Throws!"}],"data":{"thrower":null},"hasNext":true}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 57',
'',
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
'',
'-----',
'',
].join('\r\n'),
);
});

it('allows for custom error formatting in subsequent payloads of async iterator', async () => {
const app = server();

app.post(
urlString(),
graphqlHTTP({
schema: TestSchema,
customFormatErrorFn(error) {
return { message: 'Custom error format: ' + error.message };
},
}),
);

const req = app
.request()
.post(urlString())
.send({
query:
'{ test(who: "World"), ...frag @defer } fragment frag on QueryRoot { thrower }',
})
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

const response = await req;
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 46',
'',
'{"data":{"test":"Hello World"},"hasNext":true}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 105',
'',
'{"data":{"thrower":null},"path":[],"errors":[{"message":"Custom error format: Throws!"}],"hasNext":false}',
'',
'-----',
'',
].join('\r\n'),
);
});

it('allows for custom error formatting to elaborate', async () => {
const app = server();

Expand Down Expand Up @@ -2069,6 +2282,10 @@ function runTests(server: Server) {
async customExecuteFn(args) {
seenExecuteArgs = args;
const result = await Promise.resolve(execute(args));
// istanbul ignore if this test query will never return an async iterable
if (isAsyncIterable(result)) {
return result;
}
return {
...result,
data: {
Expand Down Expand Up @@ -2222,6 +2439,57 @@ function runTests(server: Server) {
});
});

it('allows for custom extensions in initial and subsequent payloads of async iterator', async () => {
const app = server();

app.post(
urlString(),
graphqlHTTP({
schema: TestSchema,
extensions({ result }) {
return { preservedResult: { ...result } };
},
}),
);

const req = app
.request()
.post(urlString())
.send({
query:
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
})
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

const response = await req;
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 124',
'',
'{"data":{"hello":"Hello Rob"},"hasNext":true,"extensions":{"preservedResult":{"data":{"hello":"Hello Rob"},"hasNext":true}}}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 148',
'',
'{"data":{"test":"Hello World"},"path":[],"hasNext":false,"extensions":{"preservedResult":{"data":{"test":"Hello World"},"path":[],"hasNext":false}}}',
'',
'-----',
'',
].join('\r\n'),
);
});

it('extension function may be async', async () => {
const app = server();

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

const response = await app
.request()
.get(urlString({ query: '{test}', raw: '' }))
.set('Accept', 'text/html');
.get(
urlString({
query:
'{ hello: test(who: "Rob"), ...frag @defer } fragment frag on QueryRoot { test(who: "World") }',
raw: '',
}),
)
.set('Accept', 'text/html')
.parse((res, cb) => {
res.on('data', (data) => {
res.text = `${res.text || ''}${data.toString('utf8') as string}`;
});
res.on('end', (err) => {
cb(err, null);
});
});

expect(response.status).to.equal(200);
expect(response.type).to.equal('application/json');
expect(response.text).to.equal('{"data":{"test":"Hello World"}}');
expect(response.type).to.equal('multipart/mixed');
expect(response.text).to.equal(
[
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 45',
'',
'{"data":{"hello":"Hello Rob"},"hasNext":true}',
'',
'---',
'Content-Type: application/json; charset=utf-8',
'Content-Length: 57',
'',
'{"data":{"test":"Hello World"},"path":[],"hasNext":false}',
'',
'-----',
'',
].join('\r\n'),
);
});
});
}
Loading

0 comments on commit 3505ce2

Please sign in to comment.