diff --git a/package.json b/package.json index e49c971a21..e841a4d132 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,10 @@ "express": "^4.13.3", "express-force-ssl": "^0.3.0", "express-graphql": "^0.6.1", + "express-interceptor": "^1.2.0", "graphiql": "^0.11.11", "graphql": "^0.13.2", + "graphql-crunch": "^1.1.2", "graphql-depth-limit": "^1.1.0", "graphql-relay": "^0.5.4", "graphql-tools": "^3.0.0", @@ -118,6 +120,7 @@ "lint-staged": "^3.4.0", "prettier": "^1.7", "sinon": "^1.17.2", + "supertest": "^3.1.0", "typescript": "^2.7.2", "typescript-babel-jest": "^1.0.5" }, diff --git a/src/index.js b/src/index.js index 60845b86d1..fc62d7d168 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import moment from "moment" import morgan from "artsy-morgan" import raven from "raven" import xapp from "artsy-xapp" +import crunchInterceptor from "./lib/crunchInterceptor" import { fetchLoggerSetup, fetchLoggerRequestDone, @@ -99,6 +100,7 @@ async function startApp() { }, logQueryDetailsIfEnabled(), fetchPersistedQuery, + crunchInterceptor, graphqlHTTP((req, res) => { const accessToken = req.headers["x-access-token"] const userID = req.headers["x-user-id"] diff --git a/src/lib/__tests__/crunchInterceptor.test.js b/src/lib/__tests__/crunchInterceptor.test.js new file mode 100644 index 0000000000..9a88487705 --- /dev/null +++ b/src/lib/__tests__/crunchInterceptor.test.js @@ -0,0 +1,45 @@ +import request from "supertest" +import { crunch } from "graphql-crunch" +import { app, invokeError } from "../../test/gql-server" +import { mockInterceptor } from "../../test/interceptor" + +import crunchInterceptor, { interceptorCallback } from "../crunchInterceptor" + +const fakeCrunch = intercept => + mockInterceptor(interceptorCallback, { + intercept, + }) + +describe("crunchInterceptor", () => { + it("should pass the result through unchanged when no param is present", () => { + const intercept = jest.fn() + return request(app(fakeCrunch(intercept))) + .get("/?query={greeting}") + .set("Accept", "application/json") + .expect(200) + .then(() => { + expect(intercept).not.toHaveBeenCalled() + }) + }) + + it("should crunch the result when param is present", () => { + return request(app(crunchInterceptor)) + .get("/?query={greeting}&crunch") + .set("Accept", "application/json") + .expect(200) + .then(res => { + expect(res.body.data).toMatchObject(crunch({ greeting: "Hello World" })) + }) + }) + + it("should not try to crunch on an error", () => { + const intercept = jest.fn() + return request(app(invokeError(404), fakeCrunch(intercept))) + .get("/?query={greeting}&crunch") + .set("Accept", "application/json") + .expect(404) + .then(() => { + expect(intercept).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/lib/crunchInterceptor.js b/src/lib/crunchInterceptor.js new file mode 100644 index 0000000000..faead63d42 --- /dev/null +++ b/src/lib/crunchInterceptor.js @@ -0,0 +1,15 @@ +import { crunch } from "graphql-crunch" +import interceptor from "express-interceptor" + +export const interceptorCallback = req => ({ + isInterceptable: () => req.query.hasOwnProperty("crunch"), + intercept: (body, send) => { + body = JSON.parse(body) // eslint-disable-line no-param-reassign + if (body && body.data) { + body.data = crunch(body.data) // eslint-disable-line no-param-reassign + } + send(JSON.stringify(body)) + }, +}) + +export default interceptor(interceptorCallback) diff --git a/src/test/gql-server.js b/src/test/gql-server.js new file mode 100644 index 0000000000..b2b680872c --- /dev/null +++ b/src/test/gql-server.js @@ -0,0 +1,39 @@ +import graphqlHTTP from "express-graphql" +import express from "express" +import bodyParser from "body-parser" +import { makeExecutableSchema, addMockFunctionsToSchema } from "graphql-tools" + +export const invokeError = status => (req, res, next) => { + const err = new Error() + err.status = status + next(err) +} + +const exampleSchema = ` + type Query { + greeting: String + }` + +export const gqlServer = ({ + schema = exampleSchema, + mocks = {}, + middleware = [], +}) => { + const app = express() + const execSchema = makeExecutableSchema({ + typeDefs: schema, + }) + addMockFunctionsToSchema({ schema: execSchema, mocks }) + app.use( + "/", + bodyParser.json(), + ...middleware, + graphqlHTTP({ + schema: execSchema, + graphiql: false, + }) + ) + return app +} + +export const app = (...middleware) => gqlServer({ middleware }) diff --git a/src/test/interceptor.js b/src/test/interceptor.js new file mode 100644 index 0000000000..8a34e2a822 --- /dev/null +++ b/src/test/interceptor.js @@ -0,0 +1,7 @@ +import interceptor from "express-interceptor" + +export const mockInterceptor = (interceptorCallback, fakeInterceptorOptions) => + interceptor((req, res) => ({ + ...interceptorCallback(req, res), + ...fakeInterceptorOptions, + })) diff --git a/yarn.lock b/yarn.lock index da8b8367b1..bf0acaed1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1649,6 +1649,12 @@ color-name@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" +combined-stream@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" + dependencies: + delayed-stream "~1.0.0" + combined-stream@^1.0.5, combined-stream@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" @@ -1675,7 +1681,7 @@ compare-versions@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.1.0.tgz#43310256a5c555aaed4193c04d8f154cf9c6efd5" -component-emitter@^1.2.1, component-emitter@~1.2.0: +component-emitter@^1.2.0, component-emitter@^1.2.1, component-emitter@~1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -1752,6 +1758,10 @@ cookiejar@2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe" +cookiejar@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -2348,6 +2358,12 @@ express-graphql@^0.6.1: http-errors "^1.3.0" raw-body "^2.1.0" +express-interceptor@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/express-interceptor/-/express-interceptor-1.2.0.tgz#33460a8e11dce7e5a022caf555d377e45ddb822a" + dependencies: + debug "^2.2.0" + express@^4.13.3: version "4.16.2" resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" @@ -2400,7 +2416,7 @@ extend@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" -extend@~3.0.0, extend@~3.0.1: +extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -2584,6 +2600,14 @@ form-data@1.0.0-rc3: combined-stream "^1.0.5" mime-types "^2.1.3" +form-data@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" + dependencies: + asynckit "^0.4.0" + combined-stream "1.0.6" + mime-types "^2.1.12" + form-data@~2.1.1: version "2.1.4" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" @@ -2606,6 +2630,10 @@ formatio@1.1.1: dependencies: samsam "~1.1" +formidable@^1.1.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" + formidable@~1.0.14: version "1.0.17" resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559" @@ -2800,6 +2828,10 @@ graphiql@^0.11.11: codemirror-graphql "^0.6.11" markdown-it "^8.4.0" +graphql-crunch@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/graphql-crunch/-/graphql-crunch-1.1.2.tgz#8c8e686d1cb92b55f779fdf902c99ac644280c15" + graphql-depth-limit@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz#59fe6b2acea0ab30ee7344f4c75df39cc18244e8" @@ -4357,7 +4389,7 @@ mersenne-twister@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mersenne-twister/-/mersenne-twister-1.1.0.tgz#f916618ee43d7179efcf641bec4531eb9670978a" -methods@~1.1.1, methods@~1.1.2: +methods@^1.1.1, methods@~1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -4419,6 +4451,10 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -5024,6 +5060,10 @@ qs@^5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.1.tgz#801fee030e0b9450d6385adc48a4cc55b44aedfc" +qs@^6.5.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" @@ -5143,6 +5183,18 @@ readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable string_decoder "~1.0.3" util-deprecate "~1.0.1" +readable-stream@^2.0.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" @@ -5806,6 +5858,12 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -5844,6 +5902,21 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" +superagent@3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + superagent@^1.2.0: version "1.8.5" resolved "https://registry.yarnpkg.com/superagent/-/superagent-1.8.5.tgz#1c0ddc3af30e80eb84ebc05cb2122da8fe940b55" @@ -5860,6 +5933,13 @@ superagent@^1.2.0: readable-stream "1.0.27-1" reduce-component "1.0.1" +supertest@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.1.0.tgz#f9ebaf488e60f2176021ec580bdd23ad269e7bc6" + dependencies: + methods "~1.1.2" + superagent "3.8.2" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"