-
Notifications
You must be signed in to change notification settings - Fork 89
Integrate graphql crunch #1042
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Integrate graphql crunch #1042
Conversation
src/index.js
Outdated
| isProduction, | ||
| }), | ||
| formatResponse: resp => { | ||
| if (req.query.crunch && resp.data && !resp.data.__schema) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only apply when
crunchis supplied as a query param- There's actual data to return
- The current response isn't an introspection query
|
This looks cool! I like the 'opt-in' pattern, it can thus be safely deployed w/o any feature flag (although a spec for the behavior as you mention would be nice). We certainly have lots of deeply nested queries so it would be interesting to see what the results of some benchmarks look like. Congrats on the first PR! |
|
I was going down the path of writing tests and ran into a few interesting issues.
The ultimate solution I came up with isn't quite as hacky feeling as the things mentioned in the end of 2, but I'd definitely value input on the approach. I found a library called express-interceptor which gives you the ability to intercept and alter the body of a response before it's returned from express. This allows us to add the middleware before As for testing, I added a library called supertest that allows the assertion of a request to express or similar http frameworks. It's an integration test of sorts because it tests the flow from |
| .get("/?query={__schema{types{name}}}&crunch") | ||
| .set("Accept", "application/json") | ||
| .expect(res => { | ||
| expect(Array.isArray(res.body.data)).toBeFalsy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expectation is that a crunched result will be an array whereas a non-crunched result will be an object. This isn't a very sophisticated test, but it at least provides some level of validation. I'm not convinced this should even be a thing though... more on that below.
src/lib/crunchInterceptor.js
Outdated
| } | ||
| send(JSON.stringify(body)) | ||
| }, | ||
| })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can anyone see why checking for __schema here would be necessary? That part comes more or less straight from the docs, but I'm assuming if the client was adding a crunch param then it already has immediate crunch handling before the results are actually processes by the client's gql library. Also, it doesn't seem like an exhaustive search for introspection queries... I mean, there's a lot of other queryable fields, right? At the very least it doesn't hurt anything so I left it there for discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also there are two other things that I'm a bit concerned with here.
- parsing the body at the beginning of the interceptor and stringifying it at the end seems like it could negatively impact perf
- I'm not sure if the interceptor will be triggered on an error response. I likely need to add a more robust check in the
isInterceptablemethod.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm pretty sure the __schema comes from the official recommendation on generating a full introspection query, basically to generate the data that fills GraphiQL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, good point, those clients wouldn't be adding the crunch param, so wouldn't see it at all
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even if a client did do an introspection query with the crunch flag, so long as the uncrunch step happened on the client before it was passed to the graphql layer I'd think that it wouldn't matter. I reached out to @jamesreggio, maybe I'm just missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there — just confirmed that including the __schema condition was an unnecessary mistake.
We built graphql-crunch as a drop-in replacement for graphql-deduplicator, which is a lossy compressor that depends upon Apollo-specific behavior. The graphql-deduplicator example code includes that condition, and we accidentally cargo-culted it into our README.
All that to say, it's safe to remove.
Also, hey @orta 👋
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense 👋
|
New dependencies added: express-interceptorAuthor: AxiomZen Description: A tiny interceptor for Express responses Homepage: https://github.com/axiomzen/express-interceptor#readme
|
| Created | 3 months ago |
| Last Updated | 3 months ago |
| License | MIT |
| Maintainers | 1 |
| Releases | 6 |
| Keywords | graphql, apollo, crunch, compress, deduplicator and normalize |
supertest
Author: TJ Holowaychuk
Description: SuperAgent driven library for testing HTTP servers
Homepage: https://github.com/visionmedia/supertest#readme
| Created | almost 6 years ago |
| Last Updated | 3 days ago |
| License | MIT |
| Maintainers | 5 |
| Releases | 38 |
| Direct Dependencies | methods and superagent |
| Keywords | superagent, request, tdd, bdd, http, test and testing |
README
SuperTest

HTTP assertions made easy via superagent.
About
The motivation with this module is to provide a high-level abstraction for testing
HTTP, while still allowing you to drop down to the lower-level API provided by superagent.
Getting Started
Install SuperTest as an npm module and save it to your package.json file as a development dependency:
npm install supertest --save-dev
Once installed it can now be referenced by simply calling require('supertest');
Example
You may pass an http.Server, or a Function to request() - if the server is not
already listening for connections then it is bound to an ephemeral port for you so
there is no need to keep track of ports.
SuperTest works with any test framework, here is an example without using any
test framework at all:
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/user', function(req, res) {
res.status(200).json({ name: 'john' });
});
request(app)
.get('/user')
.expect('Content-Type', /json/)
.expect('Content-Length', '15')
.expect(200)
.end(function(err, res) {
if (err) throw err;
});Here's an example with mocha, note how you can pass done straight to any of the .expect() calls:
describe('GET /user', function() {
it('respond with json', function(done) {
request(app)
.get('/user')
.set('Accept', 'application/json')
.expect('Content-Type', /json/)
.expect(200, done);
});
});One thing to note with the above statement is that superagent now sends any HTTP
error (anything other than a 2XX response code) to the callback as the first argument if
you do not add a status code expect (i.e. .expect(302)).
If you are using the .end() method .expect() assertions that fail will
not throw - they will return the assertion as an error to the .end() callback. In
order to fail the test case, you will need to rethrow or pass err to done(), as follows:
describe('POST /users', function() {
it('responds with json', function(done) {
request(app)
.post('/users')
.send({name: 'john'})
.set('Accept', 'application/json')
.expect(200)
.end(function(err, res) {
if (err) return done(err);
done();
});
});
});You can also use promises
describe('GET /users', function() {
it('responds with json', function() {
return request(app)
.get('/users')
.set('Accept', 'application/json')
.expect(200)
.then(response => {
assert(response.body.email, '[email protected]')
})
});
});Expectations are run in the order of definition. This characteristic can be used
to modify the response body or headers before executing an assertion.
describe('POST /user', function() {
it('user.name should be an case-insensitive match for "john"', function(done) {
request(app)
.post('/user')
.send('name=john') // x-www-form-urlencoded upload
.set('Accept', 'application/json')
.expect(function(res) {
res.body.id = 'some fixed id';
res.body.name = res.body.name.toUpperCase();
})
.expect(200, {
id: 'some fixed id',
name: 'john'
}, done);
});
});Anything you can do with superagent, you can do with supertest - for example multipart file uploads!
request(app)
.post('/')
.field('name', 'my awesome avatar')
.attach('avatar', 'test/fixtures/avatar.jpg')
...Passing the app or url each time is not necessary, if you're testing
the same host you may simply re-assign the request variable with the
initialization app or url, a new Test is created per request.VERB() call.
request = request('http://localhost:5555');
request.get('/').expect(200, function(err){
console.log(err);
});
request.get('/').expect('heya', function(err){
console.log(err);
});Here's an example with mocha that shows how to persist a request and its cookies:
const request = require('supertest');
const should = require('should');
const express = require('express');
const cookieParser = require('cookie-parser');
describe('request.agent(app)', function() {
const app = express();
app.use(cookieParser());
app.get('/', function(req, res) {
res.cookie('cookie', 'hey');
res.send();
});
app.get('/return', function(req, res) {
if (req.cookies.cookie) res.send(req.cookies.cookie);
else res.send(':(')
});
const agent = request.agent(app);
it('should save cookies', function(done) {
agent
.get('/')
.expect('set-cookie', 'cookie=hey; Path=/', done);
});
it('should send cookies', function(done) {
agent
.get('/return')
.expect('hey', done);
});
})There is another example that is introduced by the file agency.js
API
You may use any superagent methods,
including .write(), .pipe() etc and perform assertions in the .end() callback
for lower-level needs.
.expect(status[, fn])
Assert response status code.
.expect(status, body[, fn])
Assert response status code and body.
.expect(body[, fn])
Assert response body text with a string, regular expression, or
parsed body object.
.expect(field, value[, fn])
Assert header field value with a string or regular expression.
.expect(function(res) {})
Pass a custom assertion function. It'll be given the response object to check. If the check fails, throw an error.
request(app)
.get('/')
.expect(hasPreviousAndNextKeys)
.end(done);
function hasPreviousAndNextKeys(res) {
if (!('next' in res.body)) throw new Error("missing next key");
if (!('prev' in res.body)) throw new Error("missing prev key");
}.end(fn)
Perform the request and invoke fn(err, res).
Notes
Inspired by api-easy minus vows coupling.
License
MIT
Generated by 🚫 dangerJS
I'd accidentally added graphql-crunch as a dev dependency. It'd be a direct dependency in this case.
|
Okay, I think this is ready for final review. I cleaned up the Let me know what you think. |
|
This looks awesome to me, with the opt-in and tested functionality I'm 👍 to merging and maybe looking at some benchmarks! Test-wise, also looks great to me! I guess our existing tests aren't actually integration-y in that we just run queries directly against the schema using But that's just wishful thinking....this is an impressive PR! 😎 |
|
I think there's a lot we could do around testing in general. It's something I'd love to talk more about after I start. Until then, this has been a fun intro to metaphysics. 😄 |
|
ok, merging! 😎 Great work! |
|
I’d love to see a benchmark against Emission/Relay using e.g. the Home query (and thus also adding support to Emission/Relay for consuming crunched responses). |
|
I'll open a PR. |



Graphql Crunch is a library that flattens deeply nested object hierarchies into an array and deduplicates references in the array. This could yield some nice performance benefits, especially when used with complex queries.
This implementation follows their docs example and only crunches when the request has the
crunchquery param. It could just as easily be added as a header though. Ultimately this allows for incremental adoption across the Artsy clients.I'd like to add some sort of validation around this functionality and it'd also be beneficial to benchmark against some actual Artsy queries.