require 'rails/activerecord' require 'graphql' require "graphql/active_record/version" ## # This class allows GraphQL to translate ActiveRecord objects into # GraphQL objects easily and does an auto-include to make it # somewhat more efficient (hopefully) class Graphql::ActiveRecord < ::GraphQL::Field def initialize(model:, type:, use_uuid: false) @model = model @use_uuid = use_uuid self.type = type self.description = "Find a #{model.name} by ID" self.arguments = { id: GraphQL::Argument.define do type !GraphQL::ID_TYPE description "Id for record" end } end ## # override of GraphQL::Field.resolve # basically just provides the final object (in this case an AR model) # can check RDoc for graphql-ruby gem # # @param object not used # @param arguments List[GraphQL::Argument] List of Arguments for the field (i.e. id, uuid, etc.) # @param ctx [GraphQL::Context] the context of the GraphQL query # # @return eager-loaded ActiveRecord model def resolve(object, arguments, ctx) includes = map_includes(@model, ctx.ast_node.selections, ctx) model_with_includes = @model.includes(*includes) if @use_uuid model_with_includes.find_by_uuid(arguments['id']) else model_with_includes.find(arguments['id']) end rescue ActiveRecord::ConfigurationError => e @model.find(arguments['id']) end private ## # generates an array for use in AR::QueryMethods.includes # allows for nested includes as well # supports fragments # # @param model [ActiveRecord::Base] an ActiveRecord model # @param selections [GraphQL::Selection] selections on the current model # @param ctx [GraphQL::Context] the context of the GraphQL query. Used to map fragments to models # # @return Array of tables to include in the query def map_includes(model, selections, ctx) selections.map do |selection| table_name, node = handle_fragments(selection, ctx) if node.present? && node.selections.present? singular = table_name.singularize.to_sym plural = table_name.pluralize.to_sym # make sure that the next model is an ActiveRecord model next_model = singular.to_s.classify.safe_constantize if next_model.blank? || !(next_model < ActiveRecord::Base) next_model = model end nested = map_includes(next_model, node.selections, ctx) final_type = if model.reflections[singular].present? # this is for has_one relationships singular elsif model.reflections[plural].present? # this is for has_many relationships plural else # this is for non-AR models (i.e. Product) # so they use the correct parent # TODO Try to get possible_types and include those nil end if nested.present? if final_type # flatten is used for cases like Interfaces # i.e. Customer -> Product -> Loan # Product isn't an AR type { final_type => nested.flatten } else nested.compact end else final_type end else nil end end.compact end ## # method to handle fragments in GQL Query # works by finding the fragment reference in the context # # @param node [GraphQL::Language::Nodes::*] current node # @param ctx [GraphQL::Context] GQL Context used to map fragment to node # # @return [String, GraphQL::Language::Nodes::*] tuple of node name and node def handle_fragments(node, ctx) if node.class == GraphQL::Language::Nodes::FragmentSpread fragment = ctx.query.fragments[node.name] [fragment.type.underscore, fragment] elsif node.class == GraphQL::Language::Nodes::InlineFragment ## # avoid auto-including things in InlineFragments # If using an interface, not all fields are guaranteed to # exist in all of the implementing types, but at this point, # we can't tell if it will, because we don't know what type # we are dealing with. ## [nil, nil] else [node.name, node] end end end