Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GraphQL Persisted Queries with HTTP Caching

GraphQL Persisted Queries with HTTP Caching

This talk will cover a bit of history regarding Persisted GraphQL Queries, along with the problems it solves. We will look at how to implement persisted queries in Rails and Express. As an extension to persisted queries, we will look at how to adapt them to take advantage of HTTP caching.

Kevin Jalbert

July 11, 2018
Tweet

Other Decks in Programming

Transcript

  1. Who Am I • Kevin Jalbert • Work at theScore

    • Team Lead for the Emerging Platform Team • Using Rails/React and GraphQL • See more of what I'm doing: • github.com/kevinjalbert • twitter/kevinjalbert • kevinjalbert.com Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 2
  2. Agenda • Problems with GraphQL • GraphQL Persisted Queries •

    Implementing Persisted Queries • Start from scratch • Iterative approach • Using React/Apollo, Express, and Rails • Add HTTP Caching Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 3
  3. Problems with GraphQL • GraphQL APIs present some security/performance concerns

    • Exploiting circular/nested queries • Sending inefficient queries • Potentially large query being sent over the network Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 4
  4. Possible Solutions • Max Stoiber's article on Securing Your GraphQL

    API from Malicious Queries: • Depth Limiting: Rejecting queries which are too deeply nested • Amount Limiting: Rejecting queries which ask for too much information • Query Cost Analysis: Rejecting queries which are too expensive • Query Whitelisting: Rejecting queries that are not whitelisted • We are going to focus on Query Whitelisting -- a.k.a Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 5
  5. When to use Persisted Queries • Want to secure your

    API from bad queries • Gain efficiency on the network layer • Works best when you: • Can lock your production GraphQL API to only use persisted queries • Own both the server and client (i.e., not a public API) • You still have the GraphQL API flexible during development Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 6
  6. Using Persisted Queries • A client sends queryId instead of

    actual query string • The server does a lookup using queryId against set of persisted queries • If a query is found, executes it • If a query is not found, return error { operationName: "CompanyQuery", queryId: "a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80" } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 8
  7. Let's Build it! • We will be using the following:

    • React with Apollo Client • Express with GraphQL Yoga • Rails with GraphQL Ruby • Follow along kevinjalbert/graphql-persisted-queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 9
  8. // server.js const { GraphQLServer } = require('graphql-yoga') const {

    typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, endpoint: '/graphql', } server.start(options, ({ port }) => console.log( `Server started, listening on port ${port} for incoming requests.`, ), ) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 11
  9. // index.js // ... Additional imports import { ApolloClient, InMemoryCache

    } from 'apollo-boost'; import { ApolloProvider } from 'react-apollo'; import { createHttpLink } from 'apollo-link-http'; const client = new ApolloClient({ link: createHttpLink({ uri: 'http://localhost:5000/graphql' }), cache: new InMemoryCache(), }); const AppWithProvider = () => ( <ApolloProvider client={client}> <App /> </ApolloProvider> ); ReactDOM.render(<AppWithProvider />, document.getElementById('root')); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 13
  10. // components/ConsoleContainer.js import { Query } from 'react-apollo'; import QUERY

    from '../graphql/ConsolesByYear.graphql'; const ConsolesAndCompany = ({ afterYear, beforeYear }) => ( <Query query={QUERY} variables={{ afterYear, beforeYear }}> {({ data, error, loading }) => { if (error) return 'Error!'; if (loading) return 'Loading'; return ( <React.Fragment> { data.consoles.map(console => ( <div key={console.name}> <!-- details about console --> </div> )) } </React.Fragment> ); }} </Query> ); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 14
  11. Refactor React Application to use Persisted Queries Kevin Jalbert -

    GraphQL Persisted Queries with HTTP Caching 15
  12. // index.js import { createPersistedQueryLink } from 'apollo-link-persisted-queries'; // ...

    rest of file const client = new ApolloClient({ link: createPersistedQueryLink().concat( createHttpLink({ uri: 'http://localhost:5000/graphql' }) ), cache: new InMemoryCache(), }); // ... rest of file Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 16
  13. { extensions: { persistedQuery: { version: 1, sha256Hash: "a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80" }

    }, operationName: "ConsolesByYear", variables: { afterYear: 1990, beforeYear: 1999 } } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 17
  14. persistgraphql • An Apollo tool that extracts queries from a

    client and dumps them to a JSON file. { "<query_string_1>": 1, "<query_string_2>": 2, "<query_string_3>": 3, } • Unfortunately, queries are identified by an auto-incrementing ID Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 19
  15. Fixing persistgraphql • Wrap persistgraphql and calculates SHA256 for each

    query Processes .graphql files under the provided input path and outputs and/or syncs the extracted queries. The signature for each query is the result of SHA256 hashing the query's content and using the hex digest. node index.js --input-path=../react-graphql/src --output-file=./extracted_queries.json Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 20
  16. Refactor Express Server to use Persisted Queries Kevin Jalbert -

    GraphQL Persisted Queries with HTTP Caching 21
  17. const { GraphQLServer } = require('graphql-yoga') const bodyParser = require('body-parser');

    const { typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const { persistedQueriesMiddleware } = require('./persistedQueriesMiddleware') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, endpoint: '/graphql', } server.express.use(bodyParser.json()); server.express.post('/graphql', persistedQueriesMiddleware) server.start(options, ({ port }) => console.log(`Server started, on port ${port}`)) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 22
  18. // persistedQueriesMiddleware.js const { invert } = require('lodash'); const extractedQueries

    = invert(require('./extracted_queries.json')) persistedQueriesMiddleware = (req, res, next) => { console.log("Handling request to: " + req.url) const querySignature = req.body.extensions.persistedQuery.sha256Hash; const persistedQuery = extractedQueries[querySignature] if (!persistedQuery) { res.status(400).json({ errors: ['Invalid querySignature'] }) return next(new Error('Invalid querySignature')) } req.body.query = persistedQuery next() } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 23
  19. Reflection • Persisted queries between React application and Express GraphQL

    API • Main issue is using a JSON file on the server for the extracted queries • Different clients will have different JSON • Tracking/merging/juggling JSON files • Redeploying server to see changes • We can do better if we synchronize (i.e., client registers) queries to a running server Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 24
  20. rails new rails-graphql rails generate graphql:install • Basic Rails setup

    • Setup Rails Database migration/models • Setup GraphQL types • Follow graph-ruby guides Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 26
  21. # app/controllers/graphql_controller.rb class GraphqlController < ApplicationController def execute variables =

    ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) render json: result end private def ensure_hash(ambiguous_param) # ... Generated code provided by graphql-ruby's graphql:install end end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 27
  22. # app/models/persisted_query.rb class PersistedQuery < ApplicationRecord end # db/migrate/20180617011135_create_persisted_queries.rb class

    CreatePersistedQueries < ActiveRecord::Migration[5.2] def change create_table :persisted_queries do |t| t.string :signature, index: { unique: true } t.string :query t.timestamps end end end # config/routes.rb Rails.application.routes.draw do post "/graphql_persist", to: "graphql_persist#execute" end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 29
  23. # app/controllers/graphql_persist_controller.rb class GraphqlPersistController < ApplicationController def execute document =

    GraphQL.parse(params[:query]) if valid_query?(document) persisted_query = PersistedQuery.create(signature: params[:signature], query: params[:query]) render json: persisted_query.attributes else render json: { errors: @errors }, status: 500 end end private def valid_query?(document) query = GraphQL::Query.new(RailsGraphqlSchema, document: document) validator = GraphQL::StaticValidation::Validator.new(schema: RailsGraphqlSchema) results = validator.validate(query) errors = results[:errors] || [] @errors = errors.map(&:message) @errors.empty? end end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 30
  24. persistgraphql-signature-sync It is possible to sync the persisted queries to

    a specified endpoint. The endpoint needs to accept a POST request with body parameters of the query and the signature. node index.js --input-path=../react-graphql/src --sync-endpoint=http://localhost:3000/graphql_persist Synching persisted query a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80 { id: 1, signature: 'a38e6d5349901b395334b5fd3b14e84a7ca7c4fc060a4089f2c23b5cf76f0f80', query: 'query ConsolesByYear($afterYear: Int, $beforeYear: Int) { ......', created_at: '2018-07-03T19:52:54.717Z', updated_at: '2018-07-03T19:52:54.717Z' } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 31
  25. Refactor Rails Server to use Persisted Queries Kevin Jalbert -

    GraphQL Persisted Queries with HTTP Caching 32
  26. # app/controllers/graphql_controller.rb class GraphqlController < ApplicationController def execute variables =

    ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] if query.present? result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) else signature = params.dig(:extensions, :persistedQuery, :sha256Hash) persisted_query = PersistedQuery.find_by!(signature: signature) result = RailsGraphqlSchema.execute(persisted_query.query, variables: variables, operation_name: operation_name) end render json: result end # ... rest of class end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 33
  27. Reflection • Persisted queries are working with Rails GraphQL server

    • Ability to synchronize queries from clients • Future work: • Authentication on persisting endpoint • Persisted queries only in production • graphql-ruby has a pro version which comes with its own persisted query solution Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 34
  28. Let's add some HTTP Caching • We need to move

    from POST to GET requests • Let's not worrying about complicated cache rules • 10-second blanket cache • Assuming we don't have personalized data in responses Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 35
  29. Add HTTP Caching to React Application const client = new

    ApolloClient({ link: createPersistedQueryLink({ useGETForHashedQueries: true }).concat( createHttpLink({ uri: 'http://localhost:5000/graphql' }) ), cache: new InMemoryCache(), }); Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 36
  30. Add HTTP Caching to Express Server Kevin Jalbert - GraphQL

    Persisted Queries with HTTP Caching 37
  31. // server.js const { GraphQLServer } = require('graphql-yoga') const {

    typeDefs } = require('./graphql/typeDefs') const { resolvers } = require('./graphql/resolvers') const { persistedQueriesMiddleware } = require('./persistedQueriesMiddleware') const server = new GraphQLServer({ typeDefs, resolvers }) const options = { port: 5000, getEndpoint: true, endpoint: '/graphql', } server.express.get('/graphql', persistedQueriesMiddleware) server.start(options, ({ port }) => console.log(`Server started, on port ${port}`)) Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 38
  32. // persistedQueriesMiddleware.js const { invert } = require('lodash'); const extractedQueries

    = invert(require('./extracted_queries.json')) persistedQueriesMiddleware = (req, res, next) => { console.log('Handling request to: ' + req.url) res.set('Cache-Control', 'public, max-age=10') const extensions = JSON.parse(req.query.extensions) const querySignature = extensions.persistedQuery.sha256Hash; const persistedQuery = extractedQueries[querySignature] if (!persistedQuery) { res.status(400).json({ errors: ['Invalid querySignature'] }) return next(new Error('Invalid querySignature')) } req.query.query = persistedQuery next() } Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 39
  33. Add HTTP Caching to Rails Server Kevin Jalbert - GraphQL

    Persisted Queries with HTTP Caching 40
  34. class GraphqlController < ApplicationController def execute expires_in(10.seconds, public: true) variables

    = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] if query.present? result = RailsGraphqlSchema.execute(query, variables: variables, operation_name: operation_name) else extensions = JSON.parse(params[:extensions]) || {} signature = extensions.dig("persistedQuery", "sha256Hash") persisted_query = PersistedQuery.find_by!(signature: signature) result = RailsGraphqlSchema.execute(persisted_query.query, variables: variables, operation_name: operation_name) end render json: result rescue StandardError => e render json: { errors: [e.message] } end Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 41
  35. Demo • To see the advantage of HTTP caching we

    need either: • a CDN • a reverse proxy • I have deployed both Express and Rails servers • Both have simple NGINX cache in front of them Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 42
  36. Extensions • Apollo Engine and Apollo Cache Control • FastQL

    • Automatic Persisted Queries Kevin Jalbert - GraphQL Persisted Queries with HTTP Caching 43