Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An idea for dynamically connecting GraphQL types & queries to ActiveRecord #945

Closed
AndyKriger opened this issue Sep 7, 2017 · 21 comments
Closed

Comments

@AndyKriger
Copy link

AndyKriger commented Sep 7, 2017

In a Rails application with ActiveRecord model objects, the information that would be exposed via GraphQL is already there in the model. Using macros, the ActiveRecord model could be declaratively & dynamically connected to GraphQL's type & query declaration to reduce the amount of redundant code. This is a work-in-progress and probably has much room for improvement. Not everything available in the GraphQL DSL is supported yet though the direction is straightforward. There's also room for using hooks in the module macros to let model objects customize behavior further.

Feedback on this approach would be appreciated...places for improvement or refinement, caveats, hidden gotchas (because of Ruby or Rails that have not been considered or accounted for).

Thank you

# graphql_type.rb
module GraphQLType
  def self.included(klazz)
    klazz.extend Macros
  end

  module Macros

    # used to create a GraphQL type from a declarative specification
    # @param name [String] the name of the type, defaults to the class name
    # @param description [String] a docstring for GraphQL
    # @param metadata [Boolean] true if the class can have metadata
    # @param fields [Array<Symbol>] a list of fields to expose with GraphQL
    # @return [GraphQL::ObjectType] a GraphQL object type to declare for the schema
    def graphql_type( name: self.name,
                      description:'',
                      fields: )

      # because of the way GraphQL creates objects
      # we have to reference self outside of the define block
      columns = self.columns_hash

      define_singleton_method(:graphql_type) do
        GraphQL::ObjectType.define do
          name name
          description description
          
          fields.each do |f|
            field f.to_sym, GraphQLType.convert_type(columns[f.to_s].type)
          end
        end
      end
    end

    # used to create a GraphQL query for the ActiveRecord model
    # @param name [String] the name of the type, defaults to the class name
    # @param description [String] a docstring for GraphQL
    # @param arguments [Array<Hash{Symbol:Boolean}>] a list of maps of argument names to required booleans
    # @param resolver [Proc] a method that will resolve the query
    # @return [GraphQL::Field] a GraphQL field object to use in the schema
    def graphql_query( name: self.name,
                       description: '',
                       arguments: [],
                       resolver: ) # TODO: a default proc that assumes the arguments are the AR find_by
      columns = self.columns_hash

      define_singleton_method(:graphql_query) do
        GraphQL::Field.define do
            name name
            type Types.const_get("#{name}Type")
            description description
            arguments.each do |k, v|
               # TODO: use boolean required argument value to invoke to_non_null_type
              argument k, GraphQLType.convert_type(columns[k.to_s].type)
            end
            resolve resolver
        end
      end
    end

  end

  # convert a database type to a GraphQL type
  # @param db_type [Symbol] the type returned by columns_hash[column_name].type
  # @return [GraphQL::ScalarType] a GraphQL type
  def self.convert_type db_type
    # because we're outside of a GraphQL define block we cannot use the types helper
    # we must refer directly to the built-in GraphQL scalar types
    case db_type
      when :integer
        GraphQL::INT_TYPE
      else
        GraphQL::STRING_TYPE
    end
  end

  # return [Array<Class>] a list of classes that implements this module
  def self.implementations
    Rails.application.eager_load!
    puts "implementations #{self.class}"
    ActiveRecord::Base.descendants.each.select do |clz|
      begin
        clz.included_modules.include? GraphQLType
      rescue
        # it's okay that this is empty - just covering the possibility
      end
    end
  end

end
# my_schema.rb

# create types from ActiveRecord model classes that include the GraphQLType module
# very open to any feedback on doing this differently/better
# the thought behind it is to register type constants that can be referred to easily by macro methods
GraphQLType.implementations.map(&:graphql_type).each do |t|
  Types.const_set "#{t.name}Type", t
end

MySchema = GraphQL::Schema.define do
  mutation(Types::MutationType)
  query(Types::QueryType)
end
# query_type.rb

Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  # create queries for each AR model object
  GraphQLType.implementations.each { |t| field t.name.downcase, t.graphql_query}
end
# my_model.rb

require 'graphql_type'

class MyModel < ApplicationRecord
  include GraphQLType

  graphql_type description: 'An ActiveRecord model object',
               fields: [:field_one, :field_two, :field_three]

  # for simple queries like this, the find_by Proc can probably be created dynamically in the macro
  graphql_query description: 'Return information about this ActiveRecord model object',
                arguments: { myarg: true },
                resolver: ->(_, args, _) { MyModel.find_by(myarg: args[myarg]) }
end
@AndyKriger AndyKriger changed the title An idea for connecting GraphQL types & queries to ActiveRecord An idea for dynamically connecting GraphQL types & queries to ActiveRecord Sep 7, 2017
@AndyKriger
Copy link
Author

One surprising side-effect of this approach is that the problem of reloading changes when using an interface (#929) appears to go away.

@oyeanuj
Copy link

oyeanuj commented Sep 14, 2017

@AndyKriger This looks great! Would this make it possible for the model to have multiple graphql_type?

@AndyKriger
Copy link
Author

That seems like it would work. If you passed a different name (instead of relying on the default name), you'd get a different type. What is a scenario where you would want to do that?

I've refined the design since I posted this original idea to define include/exclude fields and to handle has_many associations...

    # used to create a GraphQL type from a declarative specification
    # @param name [String] the name of the type, defaults to the class name
    # @param description [String] a docstring for GraphQL
    # @param include [Array<Symbol>] a list of fields to expose with GraphQL; if not passed, all fields are exposed otherwise only declared fields are exposed
    # @param exclude [Array<Symbol>] a list of fields to hide from GraphQL; if both include & exclude are passed then excluded fields take priority
    # @return [GraphQL::ObjectType] a GraphQL object type to declare for the schema
    def graphql_type( name: self.name,
                      description:'',
                      include: [],
                      exclude: [])

      # because of the way GraphQL creates objects
      # we have to reference self outside of the define block
      columns = self.columns_hash
      belongs_to = self.reflect_on_all_associations(:belongs_to)
      has_many = self.reflect_on_all_associations(:has_many)

      define_singleton_method(:graphql_type) do
        GraphQL::ObjectType.define do
          name name
          description description

          # this is a list of fields that never get returned
          # we remove the primary key and any foreign key columns (relationships are handled later)
          db_fields_never = ( belongs_to.map(&:association_foreign_key) + belongs_to.map(&:association_primary_key) ).uniq.map(&:to_sym)

          # figure out which database fields we are exposing
          db_fields = (include.empty? ? columns.keys : include).map(&:to_sym) - exclude - db_fields_never

          # create GraphQL fields for each exposed database field
          db_fields.each do |f|
            field f, GraphQLType.convert_type(columns[f.to_s].type)
          end

          # a type might refer to other types (ex: an Application has many Merchants and a Merchant has many Principals)
          # however, a database association might not have all it's models defined as GraphQL types
          has_many.each do |reflection|
            typename = "#{reflection.class_name}Type"
            field reflection.name, Types.const_get(typename).to_list_type if Types.constants.include? typename.to_sym
          end
        end
      end
    end

@rmosolgo
Copy link
Owner

Thanks for sharing this cool idea!

We definitely need something (or several options) like this for graphql-ruby. GraphQL::Models is something similar, I know Shopify has an in-house wrapper also, and we're currently trying out something like that for GitHub.

Similar conversations about improving the graphql-ruby schema definition experience are in #820 #871 #727. So we're all heading in the same direction :)

When it comes to changing the gem's API, I want to make sure that it's useful for non-Rails users, too. For example, there might be a flexible API at the bottom with a layer of Rails-oriented helpers on top.

@AndyKriger
Copy link
Author

I looked at the activerecord gem. There are lots of great ideas in that gem, but I wanted something with less boilerplate, less DSL, that lived within the Model classes themselves; as implemented, this is oriented towards the project I'm working on.

@swalkinshaw
Copy link
Collaborator

swalkinshaw commented Sep 19, 2017

I know Shopify has an in-house wrapper also, and we're currently trying out something like that for GitHub.

Shopify's AR/model integration is almost the opposite of this issue. We integrate it into the GraphQL type rather than the AR model.

Within a GraphQL type definition, we just set the model its "backed" by which enables features like Relay's global identification, caching, etc.

While this example is great for dynamically creating types easily, I personally feel it splits up your GraphQL type definitions and obscures them a bit.

@rmosolgo
Copy link
Owner

the opposite of this issue

Good point, the approach is from the opposite direction. But what I mean is, the problem we're trying to solve is the same: the current DSL makes it hard to implement a schema quickly!

I agree with your point about organization. In my experience, Rails models are already too busy; I don't want to add anything else to those files. I would rather have a second file which draws from the model (like @swalkinshaw described). But if you prefer a model-based approach, it's interesting to explore it, like the example above.

@AndyKriger
Copy link
Author

I can see the argument for separation of concerns. It's a style choice; for me, I prefer to see all my model-related concerns in one place. For example, in one place, I see AR validations & hooks as well as the declarations of what will be exposed via GraphQL and how. Right now, I'm working on extending the idea to generating basic CRUD easily rather than repeating the same pattern across many model classes. Most of the time, it feels like model interactions are fairly straightforward so I want a way of getting that up-and-running quickly and without a lot of repetitive boilerplate. As I said before, this is a work-in-progress on a new project so I'm experimenting as I go.

@geneeblack
Copy link

geneeblack commented Oct 25, 2017

Very cool @AndyKriger , I like it. The thought of hand coding all of our types in an app with over 100 models was a daunting task. I've added a bit to your original code to accommodate all relations (except polymorphic relations) and provide some rudimentary fallbacks if a relation is invalid. Have you started a github project on this? I'd be interested in contributing since I've some ideas for additional extensions in mind. BTW, this is running under Rails 3.2 with Ruby 2.2.8

# graphql_type.rb
module GraphQLType
    def self.included(klazz)
      klazz.extend MyMacros
    end

    module MyMacros  
      protected
      
      def graphql_type( name: self.name,
        description:"",
        include: [],
        exclude: [])


          columns = self.columns_hash

          # figure out which association fields we are exposing
          association_includes =  (include.empty? ? self.reflect_on_all_associations.map(&:name) : include).map(&:to_sym) - exclude 

          # find all relations for this model, skip ones where the association klass is invalid, as well as polymorphic associations, be cognizant of include/exclude arrays similar to dbfields
          associations = self.reflect_on_all_associations.select{|t| begin t.klass rescue next end}.select{|t| !t.options[:polymorphic] && association_includes.include?(t.name.to_sym) } 
            
          # never show foreign keys for defined associations
          db_fields_never = ( associations.map(&:association_foreign_key) + associations.map(&:options).select{|v| v.key?(:foreign_key) }.map {|x| x[:foreign_key]} ).uniq.map(&:to_sym)

          # figure out which database fields we are exposing
          db_fields = (include.empty? ? columns.keys : include).map(&:to_sym) - exclude - db_fields_never


          define_singleton_method(:graphql_type) do
            GraphQL::ObjectType.define do
            #ensure type name is unique  so it does not collide with known types
            name "#{name}_gql"
            description description

            # create GraphQL fields for each exposed database field
            db_fields.each do |f|
              t = GraphQLType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type)            
              field f, -> {t}
            end

            # create GraphQL fields for each association
            associations.each do |reflection|
              begin
                klass = reflection.klass
                typename = "#{klass.class_name.tr(":", "")}_gql"
              rescue
                next # most likely an invalid association without a class name, skip if other errors are encountered
              end                    
              # graphql_type in included module is protected, it must be exposed public on the model in order to process a model
              if klass.public_methods.include?(:graphql_type)
                # if the type is not already defined in the type cache, then define it now
                if !Types.constants.include?(typename.to_sym)
                  Types.const_set typename, klass.graphql_type
                end            
                if reflection.macro == :has_many
                  t = Types.const_get(typename).to_list_type
                else
                  t = Types.const_get(typename)
                end                
                field reflection.name, -> {t}
              end
            end
          end
        end
      end


      #arguments: [{:id, GraphQL::INT_TYPE}, {:all, GraphQL::BOOLEAN_TYPE}, {:where, GraphQL::STRING_TYPE }, {:paginate, GraphQL::BOOLEAN_TYPE}],
      def graphql_query( name: self.name,
                        description: "",
                        arguments: [:id],
                        resolver: ->(_, args, _) { 
                          find(args[:id]) 
                          }) # TODO: a default proc that assumes the arguments are the AR find_by
        columns = self.columns_hash

        define_singleton_method(:graphql_query) do
          GraphQL::Field.define do
              name name
              type Types.const_get("#{name}_gql")
              description description
              arguments.each do |k, v|
                # TODO: use boolean required argument value to invoke to_non_null_type
                argument k, GraphQLType.convert_type(columns[k.to_s].type, columns[k.to_s].sql_type)
              end
              resolve resolver
          end
        end
      end
    end


    # convert a database type to a GraphQL type
    # @param db_type [Symbol] the type returned by columns_hash[column_name].type
    # @param db_sql_type [String] the sql_type returned by columns_hash[column_name].sql_type
    # @return [GraphQL::ScalarType] a GraphQL type
    def self.convert_type db_type, db_sql_type
      # because we are outside of a GraphQL define block we cannot use the types helper
      # we must refer directly to the built-in GraphQL scalar types
      case db_type
        when :integer
          GraphQL::INT_TYPE
        when :decimal
          GraphQL::FLOAT_TYPE
        when :boolean
          GraphQL::BOOLEAN_TYPE
        when :date
          Types.const_get("DateType")
        else
          case db_sql_type #these are strings not symbols
            when "geometry", "multipolygon", "polygon"
              Types.const_get("GeometryType")
            else
              GraphQL::STRING_TYPE
          end
      end
    end

    # initialize the classes that implement the graphql_type method
    def self.implementations
      Rails.application.eager_load!
      ActiveRecord::Base.descendants.each.select do |clz|
        begin
          clz.included_modules.include?(GraphQLType) && (clz.public_methods.include?(:graphql_type) || clz.public_methods.include?(:graphql_query))
        rescue
          # it is okay that this is empty - just covering the possibility
        end
      end
    end

  end


  # include GraphQlType macro on all ActiveRecord models, only exercise on the ones that expose the public methods on the model
  ActiveRecord::Base.send(:include, GraphQLType)

@AndyKriger
Copy link
Author

I haven't started a Github project for it. For one, it's very much a work in progress with the needs of the project it's part of (which is in it's early stages) and has changed a bit since this. Also, it's internal code that I shared as a proof-of-concept. I'll discuss it with my team and see if I can make it into something more public. Would be happy to collaborate either way.

@rmosolgo
Copy link
Owner

rmosolgo commented Nov 2, 2017

Thanks again for sharing this cool idea, if you end up publishing anything, please add it to our list of related projects: http://graphql-ruby.org/related_projects.html !

@rmosolgo rmosolgo closed this as completed Nov 2, 2017
@rmosolgo
Copy link
Owner

rmosolgo commented Nov 2, 2017

(feel free to keep chatting here, but i want to close this issue so I can keep track of to-do items on this project!)

@geneeblack
Copy link

geneeblack commented Nov 10, 2017

@AndyKriger FYI, I've decided to run with your original idea and have pretty much wrapped up some functionality. I'll be structuring the code a bit more and gemifying it in the future. I'll be sure to give you credit for the original idea. I've done some rudimentary testing with the following code under Rails 3.2 and 5.0. I've no reason to think that it wouldn't work under 4.0 as well but will test more as I move forward to a gem.

Here is what I have for now. It's a bit messy since it is all crammed into one class, but I'll be breaking out modules as I move it into a gem. It was easier at this point to keep it in one initialization file.

Currently models are not instrumented for graphql generation of their corresponding graphql types unless they have their respective attribute tags present on the model for the generators:

graphql_query
graphql_mutation_create
graphql_mutation_delete
graphql_mutation_update

There is an optional:
graphql_types
attribute which allows overriding defaults for the generation of the type. (inclusion/exclusion of columns, association arrays and primary/foreign keys based on their usage for query/update/delete/create/input/output.

I've also plugged in some optional authorization of the types using meta tags when they are generated. Currently it is using an ability method on the current_user using cancan, but does a soft scan to bypass the authorization if the method does not exist. It does use the same method GraphqlType.authorized? to determine when nested associations are generated based on the presence of the public method on the model.

I've tested it under Rails 5 by pointing to an existing database, doing a dbdump and then generating the models using https://github.com/frenesim/schema_to_scaffold>.

I then adding the graphql gem, graphiql gem and configured the necessary routes and controller. Then dropped in my initializer and configured the models with the tagged attributes. Everything was fairly easy to get setup and running with a generic graphql interface for the 100+ rails models.

With the exception of the create resolver, the other default resolvers appear to work in a generic manner that allows them to be used on all models. As with your example, the default resolvers can still be overridden in the definition on the model. I've also created a method to determine the correct includes (and references in Rails 4+) to be used with deeply nested association queries that allows them to be retrieved in one batch rather than in (n+1) queries.

The type output nesting can be configured to return :flat, :shallow, or :deep nesting based on the relay specification by setting a Module variable @@connection_strategy (bad name, will change it in the future). In addition the output naming can modified using :underscore or :camelize by setting the module variable @@type_case.

This is most certainly a WIP and not up to the standards of general good code practices. But I am happy with the functionality that I've been able to support up to this point.

config/initializers/graphql_type.rb

# config/initializers/graphql_type.rb
require 'graphql'

module GraphqlType
  mattr_accessor :type_suffix
  mattr_accessor :type_prefix
  mattr_accessor :type_case
  mattr_accessor :connection_strategy
  
  @@type_suffix = "_"
  @@type_prefix = ""
  @@type_case = :camelize
  @@connection_strategy = :shallow
  
  def self.included(klazz)
    klazz.extend GraphqlType_Macros
  end

  module GraphqlType_Macros
    protected

    def graphql_types(
      name: self.name,
      query: { 
        output_type: {},
        input_type: {}
      },
      update: {
        input_type: {},
        output_type: {}
      },
      delete: {
        input_type: {},
        output_type: {}
      },
      create: {
        input_type: {},
        output_type: {}
    })
      typesuffix = method(__method__).parameters.map { |arg| eval arg[1].to_s }.hash.abs.to_i.to_s
      return GraphqlType.get_constant("#{name.upcase}#{typesuffix}_GRAPHQL_DEFAULT_TYPES") if GraphqlType.const_defined?("#{name.upcase}#{typesuffix}_GRAPHQL_DEFAULT_TYPES") 
        
      graphql_type = {}
      graphql_type[:query] = query
      graphql_type[:update] = update
      graphql_type[:delete] = delete
      graphql_type[:create] = create
      merged_graphql_type = GraphqlType.graphql_default_types.deep_merge(graphql_type)
      GraphqlType.set_constant("#{name.upcase}#{typesuffix}_GRAPHQL_DEFAULT_TYPES", merged_graphql_type)
      define_singleton_method(:graphql_types) do
        merged_graphql_type
      end
    end

    def graphql_mutation_update(
      name: self.name,
      description:"",
      resolver: -> (obj, inputs, ctx){
          item = GraphqlType.update_resolver(obj, inputs, ctx, name)
          {
            item: item
          }
      },
      scope_methods: [])


      input_type = GraphqlType.get_ar_object_with_params(name, type_key: :update, type_sub_key: :input_type)
      output_type = GraphqlType.get_ar_object_with_params(name, type_key: :update, type_sub_key: :output_type)

      define_singleton_method(:graphql_mutation_update) do
        #GraphqlType.get_query(name, description, "Update", resolver, scope_methods, input_type, output_type)
        GraphqlType.get_mutation(name, description, "Update", resolver, input_type, output_type, name.downcase, "item")
      end
    end
    
    def graphql_mutation_delete(
      name: self.name,
      description:"",
      resolver: -> (obj, inputs, ctx){
          items = GraphqlType.delete_resolver(obj, inputs, ctx, name)
          {
            total: items.length,
            items: items
          }
      },
      arguments: [],
      scope_methods: [])
      
      input_type = GraphqlType.get_ar_object_with_params(name, type_key: :delete, type_sub_key: :input_type)
      output_type = GraphqlType.get_ar_object_with_params(name, type_key: :delete, type_sub_key: :output_type).to_list_type
      define_singleton_method(:graphql_mutation_delete) do
        GraphqlType.get_delete_mutation(name, description, "Delete", resolver, arguments, scope_methods, input_type, output_type)
        #GraphqlType.get_mutation(name, description, "Delete", resolver, input_type, output_type, name.downcase, "item")
      end
    end
    
    def graphql_mutation_create( 
      name: self.name,
      description:"",
      resolver: -> (obj, args, ctx){
        item = GraphqlType.create_resolver(obj, args, ctx, name)
        {
          item: item
        }
      })
      
      input_type = GraphqlType.get_ar_object_with_params(name, type_key: :create, type_sub_key: :input_type)
      output_type = GraphqlType.get_ar_object_with_params(name, type_key: :create, type_sub_key: :output_type)

      define_singleton_method(:graphql_mutation_create) do
        GraphqlType.get_mutation(name, description, "Create", resolver, input_type, output_type, name.downcase, "item")
        #GraphqlType.get_query(name, description, "Create", resolver, scope_methods, input_type, output_type)
      end
    end

    def graphql_query(  name: self.name,
                        description: "",
                        resolver: -> (obj, args, ctx) {
                          #binding.pry
                          items = GraphqlType.query_resolver(obj, args, ctx, name)
                          {
                            items: items,
                            total: items.length
                          }

                        },
                        arguments: [],
                        scope_methods: []
                      )
                      
      input_type = GraphqlType.get_ar_object_with_params(name, type_key: :query, type_sub_key: :input_type)
      output_type = GraphqlType.get_ar_object_with_params(name, type_key: :query, type_sub_key: :output_type)
      define_singleton_method(:graphql_query) do  
        GraphqlType.get_query(name, description, "Query", resolver, arguments, scope_methods, input_type, output_type)
      end
    end
  end
  
  def self.create_resolver(obj, inputs, ctx, model_name)
    if !GraphqlType.authorized?(ctx, model_name, :create)
        raise GraphQL::ExecutionError.new("error: unauthorized access: create '#{model_name.classify}'")
    end
    model = model_name.classify.constantize   
    puts "#{model_name}.create(#{inputs[model_name.downcase].to_h})"
    item = model.new(inputs[model_name.downcase].to_h)
    begin
      if !item.valid?
        raise GraphQL::ExecutionError.new(item.errors.full_messages.join("; "))
      else
        raise GraphQL::ExecutionError.new("error: WIP, item not saved but is a valid '#{model_name.classify}'")
        #item.save!
      end
    end
    item
  end

  def self.update_resolver(obj, inputs, ctx, name)
    item = self.nested_update(ctx, name, inputs)
    item
  end

  def self.delete_resolver(obj, inputs, ctx, model_name)
    model = model_name.classify.constantize
    items = GraphqlType.query_resolver(obj, inputs, ctx, model_name)
    ids = items.collect(&:id)
    if !GraphqlType.authorized?(ctx, model_name, :update)
      raise GraphQL::ExecutionError.new("error: unauthorized access: delete '#{model_name.classify}', transaction cancelled")
    end    
    begin
      deleted_items = model.delete(ids)
    rescue => e
      raise e #GraphQL::ExecutionError.new("error: delete")
    end
    if model.methods.include?(:with_deleted)
      items.with_deleted
    else
      items
    end
  end

  # build includes list for associated tables in use in the query, skips [:nodes, :edges] and result entries while walking references
  def self.get_implied_includes(model, field_names=nil, first=true, org_field_names=nil, resolve_fields=false)
    if first
      org_field_names = field_names
      # associations fields that are on the model
      a = field_names.select{|m| model.reflect_on_all_associations.map(&:name).include?(m[:name].to_sym)}.select{|m| field_names.map{|m| m[:parent_line]}.include?(m[:line])}
      # base field names that have no parent, get the lowest number parent_line on the associated field names
      a = a.select{|o| o[:parent_line] == a.map{|v| v[:parent_line]}.sort.first}
    else
      a = field_names
    end
    final_out = []
    a.each do |b|
      out = []
        child_relations = org_field_names.select{|g| g[:parent_line] == b[:line]}
        if !child_relations.empty?
          children = GraphqlType.get_implied_includes(nil, child_relations, false, org_field_names, resolve_fields)
          if children.empty?
            out << b[:name].to_sym if ![:edges, :node].include?(b[:name].to_sym)
          else
            if ![:edges, :node].include?(b[:name].to_sym)
              out << { b[:name].to_sym => children.flatten }
            else
              out = children.flatten
            end
          end
        end
        if resolve_fields && out.empty?
          out << b[:name].to_sym
        end
        final_out << out if !out.empty?
    end
    final_out
  end

  def self.get_include_fields(ctx)
    fieldnames = []
    visitor = GraphQL::Language::Visitor.new(ctx.query.document)
    visitor[GraphQL::Language::Nodes::Field] << ->(node, parent) { fieldnames << {:line=>node.line, :parent_line=>parent.line, :parent=>parent.name, :name=>node.name} }
    visitor.visit
    fieldnames
  end

  def self.query_resolver(obj, args, ctx, name)
    obj_context = name.classify.constantize
    select_args = args[:select] || args

    if !GraphqlType.authorized?(ctx, obj_context.name, :query)
      raise GraphQL::ExecutionError.new("error: unauthorized access: #{:query} '#{obj_context.class_name.classify}'")
    end
    classmethods = []
    scope_allowed = false
    with_deleted_allowed = false
    if select_args[:scope]
      classmethods = obj_context.methods - Object.methods
      scope_allowed = classmethods.include?(select_args[:scope].to_sym)
      raise GraphQL::ExecutionError.new("error: invalid scope '#{select_args[:scope]}' specified, '#{select_args[:scope]}' method does not exist on '#{ctx.field.name.classify}'") unless scope_allowed
    end
    if select_args[:with_deleted]
      classmethods = obj_context.methods - Object.methods
      with_deleted_allowed = classmethods.include?(:with_deleted)
      raise GraphQL::ExecutionError.new("error: invalid usage of 'with_deleted', 'with_deleted' method does not exist on '#{ctx.field.name.classify}'") unless with_deleted_allowed
    end
    implied_includes = self.get_implied_includes(obj_context, self.get_include_fields(ctx))
    puts implied_includes
    # implied_selects = self.stack_parents(obj_context, self.get_fields(ctx), true, nil, true)
    if !implied_includes.empty? 
      obj_context = obj_context.includes(implied_includes)
      if Rails.version.split(".").first.to_i > 4
        obj_context = obj_context.references(implied_includes)
      end
    end
    if select_args[:ids]
        obj_context = obj_context.where(["#{obj_context.model_name.plural}.id in (?)", select_args[:ids]])
    end
    if select_args[:id]
      obj_context = obj_context.where(["#{obj_context.model_name.plural}.id = ?", select_args[:id]])
    end
    if select_args[:where]
      obj_context = obj_context.where(select_args[:where])
    end
    if with_deleted_allowed
      obj_context = obj_context.with_deleted
    end
    if scope_allowed
      obj_context = obj_context.send(select_args[:scope].to_sym)
    end
    if !select_args[:limit].nil? && select_args[:limit].to_f > 0
      obj_context = obj_context.limit(select_args[:limit])
    end
    if select_args[:offset]
      obj_context = obj_context.offset(select_args[:offset])
    end
    if select_args[:order]
      obj_context = obj_context.order(select_args[:order])
    end
    if select_args[:explain]
      obj_context = obj_context.where("explain_sql = 1")
      #raise GraphQL::ExecutionError.new(obj_context.explain)
    end
    obj_context
  end

  def self.get_default_select_arguments(model, scope_methods)
    default_arguments = [
      {:name=>:explain,   :type=>GraphQL::BOOLEAN_TYPE, :default=>nil}, 
      {:name=>:id,    :type=>GraphQL::INT_TYPE, :default=>nil}, 
      {:name=>:ids,    :type=>GraphQL::INT_TYPE.to_list_type, :default=>nil}, 
      {:name=>:limit, :type=>GraphQL::INT_TYPE, :default=>50},
      {:name=>:offset, :type=>GraphQL::INT_TYPE, :default=>nil},
      {:name=>:order,   :type=>GraphQL::STRING_TYPE, :default=>nil}, 
      {:name=>:where, :type=>GraphQL::STRING_TYPE.to_list_type, :default=>nil}
    ]

    scope_methods = scope_methods.map(&:to_sym)                        
    #.select{|m| model.method(m.to_sym).arity == 0}
    if (model.public_methods - model.instance_methods - Object.methods - ActiveRecord::Base.methods).include?(:with_deleted)
      default_arguments << {:name=>:with_deleted, :type=>GraphQL::BOOLEAN_TYPE, :default=>false}
    end
    allowed_scope_methods = []
    if scope_methods.count > 0
      scope_methods.each do |s|
        #.select{|m| model.method(m.to_sym).arity == 0}
        allowed_scope_methods << s if (model.public_methods - model.instance_methods - Object.methods - ActiveRecord::Base.methods).include?(s)
      end
      if allowed_scope_methods.count > 0
        typename = GraphqlType.get_type_case("#{GraphqlType.get_type_name(model.name)}Scope_Enum")
        if !GraphqlType.defined_constant?(typename)
          enum_type = GraphQL::EnumType.define do
            name typename
            description "scope enum for #{GraphqlType.get_type_name(model.name)}"
            allowed_scope_methods.sort.each do |s|
              value(s, "")
            end
          end
          GraphqlType.set_constant typename, enum_type
        end
        default_arguments << {:name=>:scope, :type=>GraphqlType.get_constant(typename), :default=>nil}
      end
    end
=begin
    relation_includes = model.reflect_on_all_associations.select{|t| begin t.klass rescue next end}.select{|t| !t.options[:polymorphic]}.map(&:name)
    if relation_includes.count > 0
      typename = "#{GraphqlType.get_type_name(model.name)}IncludeEnum"
      if !GraphqlType.defined_constant?(typename)
        enum_type = GraphQL::EnumType.define do
          name typename
          description "include enum for #{GraphqlType.get_type_name(model.name)}"
          relation_includes.sort.each do |s|
            value(s.to_s, "")
          end
        end
        GraphqlType.set_constant typename, enum_type
      end
      default_arguments << {:name=>:includes, :type=>GraphqlType.get_constant(typename).to_list_type, :default=>nil}
    end
=end
    default_arguments
  end
  
  def self.get_delete_mutation(name, description, operation_name, resolver, arguments, scope_methods, input_type, output_type)

      query_type_name = GraphqlType.get_type_case("#{GraphqlType.get_type_name(name)}#{operation_name}")
      return GraphqlType.get_constant(query_type_name) if GraphqlType.defined_constant?(query_type_name) 
      
      model = name.classify.constantize

      default_arguments = self.get_default_select_arguments(model, scope_methods)
      select_input_type_name = GraphqlType.get_type_case("#{GraphqlType.get_type_name(name)}SelectInput")     
      if GraphqlType.defined_constant?(select_input_type_name)
        query_input_object_type = GraphqlType.get_constant(select_input_type_name)
      else
        query_input_object_type = GraphQL::InputObjectType.define do
          name select_input_type_name
          default_arguments.each do |k|
            argument k[:name].to_sym, k[:type], k[:description], default_value: k[:default] 
          end
        end
        GraphqlType.set_constant(select_input_type_name, query_input_object_type)
      end
  
        
      ret_type = GraphQL::Relay::Mutation.define do
          name query_type_name
          #return_field :item, output_object_type
          return_field :items, output_type
          return_field :total, -> {GraphQL::INT_TYPE}

          #description description
          #input_field "input".to_sym, -> {input_object_type}
          input_field :select, -> {!query_input_object_type}
  
          resolve resolver 
        end
      GraphqlType.set_constant(query_type_name, ret_type.field)
      GraphqlType.get_constant(query_type_name)
  end

  def self.authorized?(ctx, model_name, access, roles=nil)
    model = model_name.classify.constantize
    access = access.to_sym
    #here it is checking to see if public methods are exposed on items based on the operation being performed
    if (access && access == :read) || (access && access == :query)
      access = :read 
      if !model.public_methods.include?(:graphql_query)
        return false
      end
    elsif access && access == :create
      if !model.public_methods.include?(:graphql_mutation_create)
        return false
      end
    elsif access && access == :update
      if !model.public_methods.include?(:graphql_mutation_update)
        return false
      end
    elsif access && access == :delete
      if !model.public_methods.include?(:graphql_mutation_delete)
        return false
      end
    end
    if roles && roles.length > 0
      roles.each do |r|
        if !ctx[:current_user].hash_role?(role)
          return false
        end
      end
    end

    #implementation specific, here it is using an ability method on the user class plugged into cancan
    if ctx[:current_user].public_methods.include?(:ability)
      if !ctx[:current_user].ability.can? access, model
        return false
      end
    end
    true
  end

  def self.get_query(name, description, operation_name, resolver, arguments, scope_methods, input_type, output_type)
    
    query_type_name = GraphqlType.get_type_case("#{GraphqlType.get_type_name(name)}#{operation_name}")
    return GraphqlType.get_constant(query_type_name) if GraphqlType.defined_constant?(query_type_name) 
    
    model = name.classify.constantize

    default_arguments = self.get_default_select_arguments(model, scope_methods)
    select_input_type_name = "#{GraphqlType.get_type_case(GraphqlType.get_type_name(name))}QueryInput"     
    if GraphqlType.defined_constant?(select_input_type_name)
      select_input_type = GraphqlType.get_constant(select_input_type_name)
    else
      select_input_type = GraphQL::InputObjectType.define do
        name select_input_type_name
        default_arguments.each do |k|
          argument k[:name].to_sym, k[:type], k[:description], default_value: k[:default] 
        end
      end
      GraphqlType.set_constant(select_input_type_name, select_input_type)
    end

    total_output_type_name = "#{GraphqlType.get_type_case(GraphqlType.get_type_name(name))}QueryPayload"
    if GraphqlType.defined_constant?(total_output_type_name)
      total_output_type = GraphqlType.get_constant(total_output_type_name)
    else
      total_output_type = GraphQL::ObjectType.define do
        name total_output_type_name
        if [:deep, :shallow].include?(@@connection_strategy)
          connection :items, -> {output_type.connection_type}, hash_key: :items
        else
          field :items, -> {output_type.to_list_type}, hash_key: :items
        end
        field :total, -> {GraphQL::INT_TYPE}, hash_key: :total
      end
      GraphqlType.set_constant(total_output_type_name, total_output_type)
    end

      
    ret_type = GraphQL::Field.define do
        name query_type_name
        type total_output_type
        #argument :select, -> {!select_input_type}
        default_arguments.each do |k|
          argument k[:name].to_sym, k[:type], k[:description], default_value: k[:default] 
        end

        resolve resolver 
      end
    GraphqlType.set_constant(query_type_name, ret_type)
    GraphqlType.get_constant(query_type_name)
  end
        

  def self.get_mutation(name, description, operation_name, resolver, input_type, output_type, input_name, output_name)
    mutation_type_name = GraphqlType.get_type_case("#{GraphqlType.get_type_name(name)}#{operation_name}")
    return GraphqlType.get_constant(mutation_type_name) if GraphqlType.defined_constant?(mutation_type_name)
    mutation_type = GraphQL::Relay::Mutation.define do
      name mutation_type_name
      description description
      input_field input_name.to_sym, -> {input_type}
      return_field output_name.to_sym, -> {output_type}

      resolve resolver
    end

    GraphqlType.set_constant(mutation_type_name, mutation_type.field)
    GraphqlType.get_constant(mutation_type_name)
  end

  def self.get_type_name(classname, lowercase_first_letter=false)
    str = "#{GraphqlType.type_prefix}#{classname.classify.demodulize}#{GraphqlType.type_suffix}"
    if lowercase_first_letter && str.length > 0
      str = str[0].downcase + str[1..-1]
    end
    str
  end

  def self.get_type_case(str, uppercase=true)
    if @@type_case == :camelize
      if uppercase
        str.to_s.camelize(:upper)
      else
        str.to_s.camelize(:lower)
      end
    elsif @@type_case == :underscore
      if uppercase
        self.underscore(str)
      else
        str.underscore
      end
    else
      str
    end
  end

  def self.underscore(str, upcase=true)
    if upcase
      str.split('_').map {|w| w.capitalize}.join('_')
    else
      str.underscore
    end
  end

  def self.get_constant(type_name)
    GraphqlType.const_get(type_name.upcase)
  end

  def self.set_constant(type_name, type)
    GraphqlType.const_set(type_name.upcase, type)
  end

  def self.defined_constant?(type_name)
    GraphqlType.const_defined?(type_name.upcase)
  end

  # convert a database type to a GraphQL type
  # @param db_type [Symbol] the type returned by columns_hash[column_name].type
  # @param db_sql_type [String] the sql_type returned by columns_hash[column_name].sql_type
  # @return [GraphQL::ScalarType] a GraphQL type
  def self.convert_type db_type, db_sql_type="", nullable=true
    # because we are outside of a GraphQL define block we cannot use the types helper
    # we must refer directly to the built-in GraphQL scalar types
    case db_type
      when :integer
        nullable ? GraphQL::INT_TYPE : !GraphQL::INT_TYPE
      when :decimal, :float
        nullable ? GraphQL::FLOAT_TYPE : !GraphQL::FLOAT_TYPE
      when :boolean
        nullable ? GraphQL::BOOLEAN_TYPE : !GraphQL::BOOLEAN_TYPE
      when :date, :datetime
        nullable ? GraphqlType::DATE_TYPE : !GraphqlType::DATE_TYPE
      else
        case db_sql_type.to_sym #these are strings not symbols
          when :geometry, :multipolygon, :polygon
            nullable ? GraphqlType::GEOMETRY_TYPE : !GraphqlType::GEOMETRY_TYPE
          else
            nullable ? GraphQL::STRING_TYPE : !GraphQL::STRING_TYPE
        end
    end
  end

  def self.nested_update(ctx, model_name, inputs, child_name=nil, child_id=nil, parent_name=nil, parent_id=nil, klass_name=nil)
    model = model_name.classify.constantize
    
    if !child_name.nil? && !child_id.nil? # has_many && has_one
      inputs_root = inputs
      #puts "inputs_root[:id] #{inputs_root[:id]} #{inputs_root}"
      if model.public_methods.include?(:with_deleted)
        item = model.with_deleted.where("id = ? and #{child_name.downcase}_id = ?", inputs_root[:id], child_id).first
      else
        item = model.where("id = ? and #{child_name.downcase}_id = ?", inputs_root[:id], child_id).first
      end
      raise GraphQL::ExecutionError.new("error: #{model.name} record not found for #{model.name}.id = #{inputs_root[:id]} and #{model.name}.#{child_name.downcase}_id = #{child_id}") if item.nil?
    elsif !parent_name.nil? && !parent_id.nil? # belongs_to
      inputs_root = inputs
      #puts "parent_id #{parent_id} parent_name #{parent_name} #{model_name} model.with_deleted.find(#{parent_id}).send(#{parent_name}.to_sym).id} inputs_root[:id] #{inputs_root[:id]} #{inputs_root}"
      if model.public_methods.include?(:with_deleted)
        item = model.with_deleted.find(parent_id).public_send(parent_name.to_sym) if model.with_deleted.find(parent_id).public_send(parent_name.to_sym) && model.with_deleted.find(parent_id).public_send(parent_name.to_sym).id == inputs_root[:id]
      else
        item = model.find(parent_id).public_send(parent_name.to_sym) if model.find(parent_id).public_send(parent_name.to_sym) && model.with_deleted.find(parent_id).public_send(parent_name.to_sym).id == inputs_root[:id]
      end
      raise GraphQL::ExecutionError.new("error: #{model.name}.#{parent_name} record not found for  #{model.name}.with_deleted.find(#{parent_id}).#{parent_name}_id = #{inputs_root[:id]}") if item.nil?
      model_name = klass_name
      model = klass_name.classify.constantize
    else #root query always single record, need to offeset property for object_input_type
      inputs_root = inputs[model_name.downcase]
      #puts "inputs_root[:id] #{inputs_root[:id]} #{inputs_root}"
      if model.public_methods.include?(:with_deleted)
        item = model.with_deleted.find(inputs_root[:id])
      else
        item = model.find(inputs_root[:id])
      end
      raise GraphQL::ExecutionError.new("error: #{model.name} record not found for #{model.name}.id=#{inputs[model_name.downcase][:id]}") if item.nil?
    end
    if !GraphqlType.authorized?(ctx, model.name, :update)
      raise GraphQL::ExecutionError.new("error: unauthorized access: #{:update} '#{model}', transaction cancelled")
    end

    item_associations = model.reflect_on_all_associations.select{|t| begin t.klass rescue next end}.select{|t| !t.options[:polymorphic]}
    item_association_names = item_associations.map{|m| m.name.to_s}
    input_association_names = item_association_names & inputs_root.to_h.keys
    
    item.transaction do
      #puts "***********item.update_attributes(#{inputs_root.to_h.except('id').except!(*item_association_names)})"
      #puts "***********ctx[current_user.to_sym].is_admin?(#{ctx[:current_user].is_admin?})"
      item.update_attributes(inputs_root.to_h.except('id').except!(*item_association_names))
      input_association_names.each do |ia|
        lclinput = inputs_root[ia]
        ass = item_associations.select{|a| a.name.to_s == ia}.first
        klass = ass.klass
        is_collection = ass.collection?
        belongs_to = ass.belongs_to?
        #puts "#{ass.name} #{ass.collection?} #{ass.belongs_to?}"
        #puts "#{ass.association_foreign_key} #{ass.association_primary_key} #{ass.active_record_primary_key}"
        
        if is_collection
          #puts "is_collection"
          lclinput.each do |i|
            #puts "#{klass.name}  #{i.to_h}  #{model_name.downcase} #{inputs_root[:id]}"
            self.nested_update(ctx, klass.name, i, model_name.downcase, inputs_root[:id])
          end
        elsif !is_collection && belongs_to
          #puts "belongs_to"
          #puts "self.nested_update(#{ctx}, #{model.name}, #{lclinput.to_h}, nil, nil, #{ass.name}, #{inputs_root[:id]}, #{klass.name})"
          self.nested_update(ctx, model.name, lclinput, nil, nil, ass.name, inputs_root[:id], klass.name)
        elsif !is_collection && !belongs_to #has_one
          #puts "has_one"
          #puts "self.nested_update(#{ctx}, #{klass.name}, #{lclinput.to_h}, #{model_name.downcase}, #{inputs_root[:id]})"
          self.nested_update(ctx, model.name, lclinput, nil, nil, ass.name, inputs_root[:id], klass.name)
        end
      end
    end
    item
  end

  # initialize the classes that implement the graphql_type method
  def self.implementations
    Rails.application.eager_load!
    ActiveRecord::Base.descendants.each.select do |clz|
      begin
        clz.included_modules.include?(GraphqlType) && (clz.public_methods.include?(:graphql_query) || clz.public_methods.include?(:graphql_mutation_update) || clz.public_methods.include?(:graphql_mutation_delete) || clz.public_methods.include?(:graphql_mutation_create) || clz.public_methods.include?(:graphql_types))
      rescue
        # it is okay that this is empty - just covering the possibility
      end
    end
  end

  def self.schema_queries
    fields = []
    GraphqlType.implementations.select{|t| t.public_methods.include?(:graphql_query)}.each { |t|
      #binding.pry
      fields << { :name =>GraphqlType.get_type_case(t.name, false).to_sym, :field => t.graphql_query, :model_name=>t.name, :access_type=>:query }
    }
    fields
  end

  def self.schema_mutations
    fields = []
    GraphqlType.implementations.select{|t| t.public_methods.include?(:graphql_mutation_create)}.each { |t|
      fields << {:name => GraphqlType.get_type_case("#{GraphqlType.get_type_name(t.name)}Create", false).to_sym, :field=> t.graphql_mutation_create, :model_name=>t.name, :access_type=>:create }
    }
    GraphqlType.implementations.select{|t| t.public_methods.include?(:graphql_mutation_update)}.each { |t|
      fields << {:name =>GraphqlType.get_type_case("#{GraphqlType.get_type_name(t.name)}Update", false).to_sym, :field=>t.graphql_mutation_update, :model_name=>t.name, :access_type=>:update } 
    }
    GraphqlType.implementations.select{|t| t.public_methods.include?(:graphql_mutation_delete)}.each { |t|
      fields << {:name =>GraphqlType.get_type_case("#{GraphqlType.get_type_name(t.name)}Delete", false).to_sym, :field=>t.graphql_mutation_delete, :model_name=>t.name, :access_type=>:delete }
    }
    fields
  end

  def self.model_validation_keys(name)
      model = name.classify.constantize
      validation_attributes = model.validators.select{|m| m.is_a?(ActiveModel::Validations::PresenceValidator) && !m.options[:if]}.map(&:attributes).flatten
      model.reflect_on_all_associations.select{|p| validation_attributes.include?(p.name) }.map(&:foreign_key).map(&:to_sym)  | validation_attributes & model.columns_hash.keys.map(&:to_sym)
  end

  def self.get_ar_object_with_params(name, type_key: nil, type_sub_key: nil)
    #puts "get_ar_object_with_params #{name}, type_key: #{type_key}, type_sub_key: #{type_sub_key}"
    self.get_ar_object(name, GraphqlType.get_type_params(name, type_key: type_key, type_sub_key: type_sub_key))
  end

  def self.get_ar_object(name, 
    required_attributes: [], 
    excluded_attributes: [], 
    allowed_attributes: [],
    foreign_keys: false, 
    primary_keys: false, 
    validation_keys: false, 
    association_macro: nil, 
    source_nulls: true,
    type_key: nil,
    type_sub_key: nil)

    
    #typesuffix = method(__method__).parameters.map { |arg| eval arg[1].to_s }.hash.abs.to_i.to_s
    typesuffix = "#{type_key.to_s.classify}_#{self.underscore(type_sub_key.to_s)}"
    typename = GraphqlType.get_type_case("#{GraphqlType.get_type_name(name)}#{typesuffix}")
    
    return GraphqlType.get_constant(typename) if GraphqlType.defined_constant?(typename)

    model = name.classify.constantize

    required_attributes = required_attributes.map(&:to_sym) | (validation_keys ? GraphqlType.model_validation_keys(name) : [])
    
    columns = model.columns_hash

    # figure out which association fields we are exposing
    association_includes =  (model.reflect_on_all_associations(association_macro).map(&:name)).map(&:to_sym) - excluded_attributes 

    # find all relations for this model, skip ones where the association klass is invalid, as well as polymorphic associations, be cognizant of include/exclude arrays similar to dbfields
    associations = model.reflect_on_all_associations(association_macro).select{|t| begin t.klass rescue next end}.select{|t| !t.options[:polymorphic] && association_includes.include?(t.name.to_sym) } 
    # never show foreign keys for defined associations
    db_fields_never = foreign_keys ? [] : ( associations.map(&:association_foreign_key) + associations.map(&:options).select{|v| v.key?(:foreign_key) }.map {|x| x[:foreign_key]} ).uniq.map(&:to_sym)
      
    # figure out which database fields we are exposing
    allowed_attributes = allowed_attributes.count > 0 ? allowed_attributes.map(&:to_sym) : associations.map(&:name) + columns.keys.map(&:to_sym)
    allowed_associations = (associations.map(&:name) - excluded_attributes - db_fields_never) & allowed_attributes
    db_fields = (columns.keys.map(&:to_sym) - excluded_attributes - db_fields_never) & allowed_attributes
    associations = associations.select{|m| allowed_associations.include?(m.name)}
        
    ret_type = GraphQL::InputObjectType.define do
      #ensure type name is unique  so it does not collide with known types
      name typename
      description "an input interface for the #{name} ActiveRecord model"
      # create GraphQL fields for each exposed database field
      db_fields.select{|s| (primary_keys && s.to_sym == :id)}.each do |f|
        argument f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, (source_nulls ? columns[f.to_s].null : true))}
      end       
      db_fields.select{|s| required_attributes.include?(s)}.each do |f|
        argument f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, false)}
      end       
      # create GraphQL fields for each association
      associations.sort_by(&:name).each do |reflection|
        begin
          klass = reflection.klass
        rescue
          next # most likely an invalid association without a class name, skip if other errors are encountered
        end                    
        if reflection.macro == :has_many
          argument reflection.name.to_sym, -> {GraphqlType.get_ar_object_with_params(klass.name, type_key: type_key, type_sub_key: type_sub_key).to_list_type} do
            authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
            model_name klass.name
            access_type type_key.to_s
          end   
        else
          argument reflection.name.to_sym, -> {GraphqlType.get_ar_object_with_params(klass.name, type_key: type_key, type_sub_key: type_sub_key)} do
            authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
            model_name klass.name
            access_type type_key.to_s
          end   
        end                
      end

      db_fields.reject{|s| (primary_keys && s.to_sym == :id) || required_attributes.include?(s)}.sort.each do |f|
        argument f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, (source_nulls ? columns[f.to_s].null : true))}
      end
    end if type_sub_key == :input_type

    ret_type = GraphQL::ObjectType.define do
      #ensure type name is unique  so it does not collide with known types
      name typename
      description "an output interface for the #{name} ActiveRecord model"
      # create GraphQL fields for each exposed database field
      db_fields.select{|s| (primary_keys && s.to_sym == :id)}.each do |f|
        #puts "source null #{f} #{source_nulls ? columns[f.to_s].null : true}"
        field f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, (source_nulls ? columns[f.to_s].null : true))}
      end       
      db_fields.select{|s| required_attributes.include?(s)}.sort.each do |f|
        field f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, false)}
      end       
    # create GraphQL fields for each association
      associations.sort_by(&:name).each do |reflection|
        begin
          klass = reflection.klass
        rescue
          next # most likely an invalid association without a class name, skip if other errors are encountered
        end                    
        if reflection.macro == :has_many
          if [:deep].include?(@@connection_strategy) && type_key == :query
            connection reflection.name.to_sym, -> {GraphqlType.get_ar_object_with_params(klass.name, type_key: type_key, type_sub_key: type_sub_key).connection_type} do
              authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
              model_name klass.name
              access_type :read.to_s
            end 
          else
            field reflection.name.to_sym, -> {GraphqlType.get_ar_object_with_params(klass.name, type_key: type_key, type_sub_key: type_sub_key).to_list_type} do
              authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
              model_name klass.name
              access_type :read.to_s
            end    
          end
        else
          field reflection.name.to_sym, -> {GraphqlType.get_ar_object_with_params(klass.name, type_key: type_key, type_sub_key: type_sub_key)} do
            authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
            model_name klass.name
            access_type :read.to_s
          end   
        end
      end                


      db_fields.reject{|s| (primary_keys && s.to_sym == :id) || required_attributes.include?(s)}.sort.each do |f|
        #puts "source null #{f} #{source_nulls ? columns[f.to_s].null : true}"
        field f.to_sym, -> {GraphqlType.convert_type(columns[f.to_s].type, columns[f.to_s].sql_type, (source_nulls ? columns[f.to_s].null : true))}
      end
    end if type_sub_key == :output_type


    GraphqlType.set_constant(typename, ret_type) if !GraphqlType.defined_constant?(typename)

    ret_type
  end

  def self.get_type_params(name, type_key: nil, type_sub_key: nil)
    model = name.classify.constantize
    if model.public_methods.include?(:graphql_types)
      params = model.graphql_types
    else
      params = GraphqlType.graphql_default_types
    end
    #puts params
    if !type_key.nil?
      if params.keys.include?(type_key.to_sym)
        params = params[type_key.to_sym]
        if !type_sub_key.nil?
          if params.keys.include?(type_sub_key.to_sym)
            params = params[type_sub_key.to_sym]
          else
            params = nil
          end
        end
      else
        params = nil
      end
    end
    params 
  end

  def self.graphql_default_types(
    query: {
    output_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: false,
      type_key: :query,
      type_sub_key: :output_type
    },
    input_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: false,
      type_key: :query,
      type_sub_key: :input_type
    }
  },
  update: {
    input_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: false,
      type_key: :update,
      type_sub_key: :input_type
    },
    output_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: true,
      type_key: :update,
      type_sub_key: :output_type
    }
  },
  delete: {
    input_type: {
      required_attributes: [:id], 
      excluded_attributes: [], 
      allowed_attributes: [:id], 
      foreign_keys: false, 
      primary_keys: true, 
      validation_keys: true, 
      association_macro: nil, 
      source_nulls: false,
      type_key: :delete,
      type_sub_key: :input_type
    },
    output_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: false, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: true,
      type_key: :delete,
      type_sub_key: :output_type
    }
  },
  create: {
    input_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: false, 
      validation_keys: false, 
      association_macro: :has_many, 
      source_nulls: false,
      type_key: :create,
      type_sub_key: :input_type
    },
    output_type: {
      required_attributes: [], 
      excluded_attributes: [], 
      allowed_attributes: [], 
      foreign_keys: true, 
      primary_keys: true, 
      validation_keys: false, 
      association_macro: nil, 
      source_nulls: true,
      type_key: :create,
      type_sub_key: :output_type
    }
  })
    return GraphqlType.get_constant("GRAPHQL_DEFAULT_TYPES") if GraphqlType.const_defined?("GRAPHQL_DEFAULT_TYPES") 
      
    graphql_type = {}
    graphql_type[:query] = query
    graphql_type[:update] = update
    graphql_type[:delete] = delete
    graphql_type[:create] = create

    GraphqlType.set_constant("GRAPHQL_DEFAULT_TYPES", graphql_type)
    graphql_type
  end
end


GraphqlType::GEOMETRY_TYPE = GraphQL::ScalarType.define do
    name "Geometry"
    description "The Geometry scalar type enables the serialization of Geometry data"

    coerce_input ->(value, ctx) do
        begin
            value.nil? ? nil : GeoRuby::SimpleFeatures::Geometry.from_geojson(value)
        rescue ArgumentError
            raise GraphQL::CoercionError, "cannot coerce `#{value.inspect}` to json"
        end
    end
    coerce_result ->(value, ctx) { value.nil? ? "" : value.to_json }
  end

GraphqlType::DATE_TYPE = GraphQL::ScalarType.define do
    name "Date"
    description "The Date scalar type enables the serialization of date data to/from iso8601"

    coerce_input ->(value, ctx) do
        begin
            value.nil? ? nil :  Date.iso8601(value)
        rescue ArgumentError
            raise GraphQL::CoercionError, "cannot coerce `#{value.inspect}` to date"
        end
    end
    coerce_result ->(value, ctx) { value.nil? ? nil : value.iso8601 }
  end

  GraphqlType::QueryType = GraphQL::ObjectType.define do
    name 'Query'
    # create queries for each AR model object
    field :welcomeQuery, types.String, hash_key: :welcomeQuery do
      resolve -> (obj, args, ctx){
        {
          welcomeQuery: "this is a placeholder query in case you do not have access to other queries"
        }
      }
    end
    GraphqlType.schema_queries.each do |f|
      field f[:name], f[:field]  do
        authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
        model_name f[:model_name]
        access_type f[:access_type].to_s
      end   
    end
  end

  GraphqlType::MutationType = GraphQL::ObjectType.define do
    name 'Mutation'

    GraphqlType.schema_mutations.each do |f|
      field :welcomeMutation, types.String, hash_key: :welcomeMutation do
        resolve -> (obj, args, ctx){
          {
            welcomeMutation: "this is a placeholder mutation in case you do not have access to other mutations"
          }
        }
      end

      field f[:name], f[:field]  do
        authorized ->(ctx, model_name, access_type) { GraphqlType.authorized?(ctx, model_name, access_type.to_sym) }
        model_name  f[:model_name]
        access_type f[:access_type].to_s
      end   
    end
  end

  # include GraphqlType macro on all ActiveRecord models, only exercise on the ones that expose the public methods on the model

  ActiveRecord::Base.send(:include, GraphqlType) if defined?(ActiveRecord)

app/graphql/graphqltype_schema.rb

 # app/graphql/graphqltype_schema.rb

  GraphQL::Field.accepts_definitions(
    authorized: ->(field, authorized_proc) { field.metadata[:authorized_proc] = authorized_proc }
  )
  GraphQL::Field.accepts_definitions(
    model_name: GraphQL::Define.assign_metadata_key(:model_name)
  )
  GraphQL::Field.accepts_definitions(
    access_type: GraphQL::Define.assign_metadata_key(:access_type)
  )
  GraphQL::Argument.accepts_definitions(
    authorized: ->(field, authorized_proc) { field.metadata[:authorized_proc] = authorized_proc }
  )
  GraphQL::Argument.accepts_definitions(
    model_name: GraphQL::Define.assign_metadata_key(:model_name)
  )
  GraphQL::Argument.accepts_definitions(
    access_type: GraphQL::Define.assign_metadata_key(:access_type)
  )

  GraphqltypeSchema = GraphQL::Schema.define do
    #use GraphQL::Backtrace
    default_max_page_size 100
    mutation(GraphqlType::MutationType)
    query(GraphqlType::QueryType)
  end

  log_query_depth = GraphQL::Analysis::QueryDepth.new { |query, depth| Rails.logger.info("[******GraphQL Query Depth] #{depth}") }
  GraphqltypeSchema.query_analyzers << log_query_depth
  log_query_complexity = GraphQL::Analysis::QueryComplexity.new { |query, complexity| Rails.logger.info("[******GraphQL Query Complexity] #{complexity}")}
  GraphqltypeSchema.query_analyzers << log_query_complexity

  #=begin

  GraphQL::Errors.configure(GraphqltypeSchema) do

    rescue_from ActiveRecord::StatementInvalid do |exception|
      GraphQL::ExecutionError.new(exception.message)
    end

    rescue_from ActiveRecord::RecordNotFound do |exception|
      GraphQL::ExecutionError.new(exception.message)
    end


    rescue_from ActiveRecord::RecordInvalid do |exception|
      GraphQL::ExecutionError.new(exception.message)
    end

    rescue_from StandardError do |exception|
      GraphQL::ExecutionError.new(exception.message)
    end
  end
  #=end

Gemfile

# Gemfile
....
gem 'graphql'
gem "graphiql-rails"
gem 'graphql-errors'
gem 'cancancan', '~> 1.10'

app/controllers/graphql_controller.rb

  # app/controllers/graphql_controller.rb
  class GraphqlController < ApplicationController
    def execute
      variables = ensure_hash(params[:variables])
      query = params[:query]
      operation_name = params[:operationName]
      context = {
        # Query context goes here, for example:
        #current_user: current_user
      }
      
      begin
        #if (logged_in?)# && current_user.is_admin?)
        #  Ability.new(current_user)        
        #elsif Rails.env != "development"
        #  query = nil
        #end
        result = GraphqltypeSchema.execute(query, variables: variables, context: context, operation_name: operation_name, except: ExceptFilter, max_depth:20)

      end
      render json: result
    end

    private

    class ExceptFilter
      def self.call(schema_member, context)
        #puts schema_member
        # true if field should be excluded, false if it should be included
        return false unless authorized_proc = schema_member.metadata[:authorized_proc]
        model_name = schema_member.metadata[:model_name]
        access_type = schema_member.metadata[:access_type]
        !authorized_proc.call(context, model_name, access_type)
      end
    end

    def ensure_hash(query_variables)
      if query_variables.blank?
        {}
      elsif query_variables.is_a?(String)
        JSON.parse(query_variables)
      else
        query_variables
      end
    end

  end

@dkniffin
Copy link

dkniffin commented Nov 12, 2017

@geneeblack @AndyKriger I'd also be interested in helping build and contribute to this gem. I've just copied this code into my own project, and got it working. I had to make some tweaks because my project is running mongodb/mongoid, such as changing columns_hash to fields, but overall it was surprisingly easy to plug in.

It's a shame graphql-rails is taken and abandoned, because I think that would be a great name for it. Maybe we can reach out to him when the time comes and see if he'd be willing to give up the name. Or we can come up with our own name.

Let me know when you have a repo set up, and we can start making issues to track the work.

edit: Actually, it looks like @jamesreggio is active on GH. Maybe he can chime in here on whether he'd be willing to give up this gem name?

@geneeblack
Copy link

geneeblack commented Nov 13, 2017

@dkniffin I've published a gem https://rubygems.org/gems/graphql_model_mapper/versions/0.0.5 This is my first attempt at publishing a gem, so any feedback/guidance will be appreciated. The repo is at https://github.com/geneeblack/graphql_model_mapper

If you have some ideas on how this could accommodate mongodb I'd be willing to hear them. I don't currently have any projects set up with mongodb so I don't have any readily available test cases to work with.

@dkniffin
Copy link

@geneeblack I'll take a look. Just so you know, your links aren't working right. If I copy/paste the url into the browser, it works great, but clicking the link sends me to https://github.com/rmosolgo/graphql-ruby/issues/url

@geneeblack
Copy link

@dkniffin Thanks, I changed the links to straight addresses rather than using the github link markup. they should be working now.

@AndyKriger
Copy link
Author

i'm interested in helping out with this - i have a done a lot of cleanup and improvement to my initial GraphQLType code and i got approval to put this code out into the world

@geneeblack
Copy link

@AndyKriger very cool, your help is greatly appreciated, especially since you fostered the original idea, I apologize for the state of the code in advance since ruby/rails is not my native coding language. I'm sure your improvements will bring some needed order to the project.

@AndyKriger
Copy link
Author

i'm taking the discussion over to the graphql_model_mapper project where i posted the most recent iteration of that module

@Amandeepsinghghai
Copy link

@AndyKriger Check out https://github.com/keepworks/graphql-sugar. It is basically a wrapper over graphql-ruby that cuts down on boilerplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants