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
One surprising side-effect of this approach is that the problem of reloading changes when using an interface (#929) appears to go away.
@AndyKriger This looks great! Would this make it possible for the model to have multiple graphql_type?
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
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.
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.
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.
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.
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.
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)
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.
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 !
(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!)
@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
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
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
....
gem 'graphql'
gem "graphiql-rails"
gem 'graphql-errors'
gem 'cancancan', '~> 1.10'
# 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
@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?
@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.
@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
@dkniffin Thanks, I changed the links to straight addresses rather than using the github link markup. they should be working now.
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
@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.
i'm taking the discussion over to the graphql_model_mapper project where i posted the most recent iteration of that module
@AndyKriger Check out https://github.com/keepworks/graphql-sugar. It is basically a wrapper over graphql-ruby that cuts down on boilerplate.