Created
October 13, 2021 18:22
-
-
Save rmosolgo/84eeb2700e75aa21a1fea854c608c425 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# | |
# LRU cache based on Ruby 1.9+ ordered hash inspired by Sam Saffron: https://stackoverflow.com/a/16161783 | |
# | |
# @example Caching valid query documents in a Rails controller | |
# | |
# class GraphqlController < ApplicationController | |
# # TODO: not thread-safe. See https://github.com/samsaffron/lru_redux for a thread-safe LRU cache | |
# PARSED_QUERY_CACHE = GraphQLParseAndValidateCache.new(schema: MySchema, max_entries: 20) | |
# | |
# def execute | |
# query_string = params[:query_string] | |
# ast_or_errors = PARSED_QUERY_CACHE.fetch(query_string) | |
# if ast_or_errors.is_a?(Hash) | |
# # it's an error response | |
# render json: ast_or_errors | |
# else | |
# query_document = ast_or_errors | |
# context = { ... } | |
# # Pass `validate: false` since it was validated by the cache | |
# result = MySchema.execute(query_document, validate: false, context: context, ...) | |
# render json: result | |
# end | |
# end | |
# end | |
class GraphQLParseAndValidateCache | |
def initialize(schema:, max_entries:) | |
@schema = schema | |
@max_entries = max_entries | |
@entries = {} | |
end | |
# @param query_string [String] | |
# @return [GraphQL::Language::Document, Hash] Returns a hash containing an `"errors"` key if the query string failed parsing or static validation. | |
# Returns a valid AST if the query string passed parsing and validation (and may be returned from cache). | |
# | |
def fetch(query_string) | |
found = true | |
# Delete the value if found, so we can use the ordered nature of Ruby hashes to implement LRU | |
parsed_ast = @entries.delete(query_string) { found = false } | |
if found | |
# Re-assign the value to put it at the end of the hash | |
@entries[query_string] = parsed_ast | |
else | |
# The GraphQL-Ruby API here is not great: | |
# `GraphQL.parse` raises an error if `query_string` has syntax errors; rescue it below. | |
parsed_ast = GraphQL.parse(query_string) | |
# `Schema.validate` returns an array of validation errors if it's invalid. | |
# This implementation doesn't cache invalid responses, assuming that it's an infrequent path. | |
# But it could be updated if necessary, by storing the hash created in the `else` block below. | |
validation_errors = @schema.validate(parsed_ast) | |
if validation_errors.empty? | |
@entries[query_string] = parsed_ast | |
# If this entry exceeded the max size, remove the least-recently used entry: | |
if @entries.length > @max_entries | |
least_recently_used_key = @entries.first[0] | |
@entries.delete(least_recently_used_key) | |
end | |
parsed_ast | |
else | |
{ "errors" => validation_errors.map(&:to_h) } | |
end | |
end | |
rescue GraphQL::ParseError => err | |
{ "errors" => [err.to_h] } | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment