lib/graphql/remote_loader/loader.rb in graphql-remote_loader-1.0.5 vs lib/graphql/remote_loader/loader.rb in graphql-remote_loader-1.0.6

- old
+ new

@@ -1,29 +1,26 @@ -require "prime" +# frozen_string_literal: true + require "json" require_relative "query_merger" module GraphQL module RemoteLoader class Loader < GraphQL::Batch::Loader # Delegates to GraphQL::Batch::Loader#load - # We include a unique prime as part of the batch key to use as part + # We include a unique id as part of the batch key to use as part # of the alias on all fields. This is used to # a) Avoid name collisions in the generated query # b) Determine which fields in the result JSON should be # handed fulfilled to each promise def self.load(query, context: {}, variables: {}) - @index ||= 1 + @index ||= 0 @index += 1 - prime = Prime.take(@index - 1).last - store_context(context) - interpolate_variables!(query, variables) - - self.for.load([query, prime, @context]) + self.for.load([interpolate_variables(query, variables), @index, @context]) end # Loads the value, then if the query was successful, fulfills promise with # the leaf value instead of the full results hash. # @@ -67,95 +64,108 @@ "RemoteLoader::Loader should be subclassed and #query must be defined" end private - def perform(queries_and_primes) - query_string = QueryMerger.merge(queries_and_primes).gsub(/\s+/, " ") - context = queries_and_primes[-1][-1] + def perform(queries_and_ids) + query_string = QueryMerger.merge(queries_and_ids).gsub(/\s+/, " ") + context = queries_and_ids[-1][-1] response = query(query_string, context: context).to_h data, errors = response["data"], response["errors"] - queries_and_primes.each do |query, prime, context| + queries_and_ids.each do |query, caller_id, context| response = {} - response["data"] = filter_keys_on_data(data, prime) + response["data"] = filter_keys_on_data(data, caller_id) - errors_key = filter_errors(errors, prime) + errors_key = filter_errors(errors, caller_id) response["errors"] = dup(errors_key) unless errors_key.empty? - scrub_primes_from_error_paths!(response["errors"]) + scrub_caller_ids_from_error_paths!(response["errors"]) - fulfill([query, prime, context], response) + fulfill([query, caller_id, context], response) end end # Interpolates variables into the given query. # For String variables, surrounds the interpolated string in quotes. # To interpolate a String as an Int, Float, or Bool, convert to the appropriate Ruby type. # # E.g. # interpolate_variables("foo(bar: $my_var)", { my_var: "buzz" }) # => "foo(bar: \"buzz\")" + def self.interpolate_variables(query, variables = {}) + query.dup.tap { |mutable_query| interpolate_variables!(mutable_query, variables) } + end + def self.interpolate_variables!(query, variables = {}) - variables.each do |variable, value| - case value - when Integer, Float, TrueClass, FalseClass - # These types are safe to directly interpolate into the query, and GraphQL does not expect these types to be quoted. - query.gsub!("$#{variable.to_s}", value.to_s) - else - # A string is either a GraphQL String or ID type. - # This means we need to - # a) Surround the value in quotes - # b) escape special characters in the string - # - # This else also catches unknown objects, which could break the query if we directly interpolate. - # These objects get converted to strings, then escaped. + variables.each { |variable, value| query.gsub!("$#{variable.to_s}", stringify_variable(value)) } + end - query.gsub!("$#{variable.to_s}", value.to_s.inspect) - end + def self.stringify_variable(value) + case value + when Integer, Float, TrueClass, FalseClass + # These types are safe to directly interpolate into the query, and GraphQL does not expect these types to be quoted. + value.to_s + when Array + # Arrays can contain elements with various types, so we need to check them one by one + stringified_elements = value.map { |element| stringify_variable(element) } + "[#{stringified_elements.join(', ')}]" + when Hash + # Hashes can contain values with various types, so we need to check them one by one + stringified_key_value_pairs = value.map { |key, value| "#{key}: #{stringify_variable(value)}" } + "{#{stringified_key_value_pairs.join(', ')}}" + else + # A string is either a GraphQL String or ID type. + # This means we need to + # a) Surround the value in quotes + # b) escape special characters in the string + # + # This else also catches unknown objects, which could break the query if we directly interpolate. + # These objects get converted to strings, then escaped. + + value.to_s.inspect end end def dup(hash) JSON.parse(hash.to_json) end - def filter_keys_on_data(obj, prime) + def filter_keys_on_data(obj, caller_id) case obj when Array - obj.map { |element| filter_keys_on_data(element, prime) } + obj.map { |element| filter_keys_on_data(element, caller_id) } when Hash filtered_results = {} # Select field keys on the results hash fields = obj.keys.select { |k| k.match /\Ap[0-9]+.*[^?]\z/ } # Filter methods that were not requested in this sub-query fields = fields.select do |field| - prime_factor = field.match(/\Ap([0-9]+)/)[1].to_i - (prime_factor % prime) == 0 + graphql_caller = field.match(/\Ap([0-9]+)/)[1].to_i + graphql_caller[caller_id] == 1 # Fixnum#[] accesses bitwise representation of num end - # redefine methods on new obj, recursively filter sub-selections - fields.each do |method| - method_name = method.match(/\Ap[0-9]+(.*)/)[1] + # redefine fields on new obj, recursively filter sub-selections + fields.each do |field| + field_name = field.match(/\Ap[0-9]+(.*)/)[1] - method_value = obj[method] - filtered_value = filter_keys_on_data(method_value, prime) - - filtered_results[underscore(method_name)] = filtered_value + value = obj[field] + filtered_results[underscore(field_name)] = filter_keys_on_data(value, caller_id) end filtered_results else + # Base case, no more recursion needed. return obj end end - def filter_errors(errors, prime) + def filter_errors(errors, caller_id) return [] unless errors errors.select do |error| # For now, do not support global errors with no path key next unless error["path"] @@ -163,16 +173,16 @@ # We fulfill a promise with an error object if field in the path # key was requested by the promise. error["path"].all? do |path_key| next true if path_key.is_a? Integer - path_key_prime = path_key.match(/\Ap([0-9]+)/)[1].to_i - path_key_prime % prime == 0 + path_key_caller_id = path_key.match(/\Ap([0-9]+)/)[1].to_i + path_key_caller_id[caller_id] end end end - def scrub_primes_from_error_paths!(error_array) + def scrub_caller_ids_from_error_paths!(error_array) return unless error_array error_array.map do |error| error["path"].map! do |path_key| path_key.match(/\Ap[0-9]+(.*)/)[1]