First working version of new GraphQL::TypeCreator after major refactoring
This commit is contained in:
@@ -1,12 +0,0 @@
|
|||||||
ConsulSchema = GraphQL::Schema.define do
|
|
||||||
query QueryRoot
|
|
||||||
|
|
||||||
# Reject deeply-nested queries
|
|
||||||
max_depth 7
|
|
||||||
|
|
||||||
resolve_type -> (object, ctx) {
|
|
||||||
# look up types by class name
|
|
||||||
type_name = object.class.name
|
|
||||||
ConsulSchema.types[type_name]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
class ExposableAssociation
|
|
||||||
attr_reader :name, :type
|
|
||||||
|
|
||||||
def initialize(association)
|
|
||||||
@name = association.name
|
|
||||||
@type = association.macro # :has_one, :belongs_to or :has_many
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
class ExposableField
|
|
||||||
attr_reader :name, :graphql_type
|
|
||||||
|
|
||||||
def initialize(column, options = {})
|
|
||||||
@name = column.name
|
|
||||||
@graphql_type = ExposableField.convert_type(column.type)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# Return a GraphQL type for 'database_type'
|
|
||||||
def self.convert_type(database_type)
|
|
||||||
case database_type
|
|
||||||
when :integer
|
|
||||||
GraphQL::INT_TYPE
|
|
||||||
when :boolean
|
|
||||||
GraphQL::BOOLEAN_TYPE
|
|
||||||
when :float
|
|
||||||
GraphQL::FLOAT_TYPE
|
|
||||||
when :double
|
|
||||||
GraphQL::FLOAT_TYPE
|
|
||||||
else
|
|
||||||
GraphQL::STRING_TYPE
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
class ExposableModel
|
|
||||||
attr_reader :name, :description, :exposed_fields, :exposed_associations
|
|
||||||
|
|
||||||
def initialize(model_class, options = {})
|
|
||||||
@name = model_class.name
|
|
||||||
@description = model_class.model_name.human
|
|
||||||
@field_filter_list = options[:field_filter_list] || []
|
|
||||||
@assoc_filter_list = options[:assoc_filter_list] || []
|
|
||||||
@filter_strategy = options[:filter_strategy]
|
|
||||||
set_exposed_items(model_class)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
# determine which model fields and associations are exposed to the API
|
|
||||||
def set_exposed_items(model_class)
|
|
||||||
@exposed_fields = check_against_safety_list(model_class.columns, @field_filter_list)
|
|
||||||
@exposed_associations = check_against_safety_list(model_class.reflect_on_all_associations, @assoc_filter_list)
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_against_safety_list(all_items, safety_list)
|
|
||||||
case @filter_strategy
|
|
||||||
when :whitelist
|
|
||||||
exposed_items = all_items.select do |column|
|
|
||||||
safety_list.include? column.name.to_s # works for both symbols and strings
|
|
||||||
end
|
|
||||||
when :blacklist
|
|
||||||
exposed_items = all_items.select do |column|
|
|
||||||
!(safety_list.include? column.name.to_s)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
exposed_items = []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
QueryRoot = GraphQL::ObjectType.define do
|
|
||||||
name "Query"
|
|
||||||
description "The query root for this schema"
|
|
||||||
|
|
||||||
field :proposal do
|
|
||||||
type TYPE_BUILDER.types[Proposal]
|
|
||||||
description "Find a Proposal by id"
|
|
||||||
argument :id, !types.ID
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Proposal.find(arguments["id"])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :proposals do
|
|
||||||
type types[TYPE_BUILDER.types[Proposal]]
|
|
||||||
description "Find all Proposals"
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Proposal.all
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :debate do
|
|
||||||
type TYPE_BUILDER.types[Debate]
|
|
||||||
description "Find a Debate by id"
|
|
||||||
argument :id, !types.ID
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Debate.find(arguments["id"])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :debates do
|
|
||||||
type types[TYPE_BUILDER.types[Debate]]
|
|
||||||
description "Find all Debates"
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Debate.all
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :comment do
|
|
||||||
type TYPE_BUILDER.types[Comment]
|
|
||||||
description "Find a Comment by id"
|
|
||||||
argument :id, !types.ID
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Comment.find(arguments["id"])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :comments do
|
|
||||||
type types[TYPE_BUILDER.types[Comment]]
|
|
||||||
description "Find all Comments"
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
Comment.all
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
field :user do
|
|
||||||
type TYPE_BUILDER.types[User]
|
|
||||||
description "Find a User by id"
|
|
||||||
argument :id, !types.ID
|
|
||||||
resolve -> (object, arguments, context) {
|
|
||||||
User.find(arguments["id"])
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
class TypeBuilder
|
|
||||||
attr_reader :filter_strategy, :graphql_models
|
|
||||||
attr_accessor :graphql_types # contains all generated GraphQL types
|
|
||||||
|
|
||||||
def initialize(graphql_models, options = {})
|
|
||||||
@graphql_models = graphql_models
|
|
||||||
@graphql_types = {}
|
|
||||||
|
|
||||||
# determine filter strategy for this field
|
|
||||||
if (options[:filter_strategy] == :blacklist)
|
|
||||||
@filter_strategy = :blacklist
|
|
||||||
else
|
|
||||||
@filter_strategy = :whitelist
|
|
||||||
end
|
|
||||||
|
|
||||||
create_all_types
|
|
||||||
end
|
|
||||||
|
|
||||||
def types
|
|
||||||
@graphql_types
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def create_all_types
|
|
||||||
@graphql_models.keys.each do |model_class|
|
|
||||||
@graphql_types[model_class] = create_type(model_class)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_type(model_class)
|
|
||||||
type_builder = self
|
|
||||||
|
|
||||||
graphql_type = GraphQL::ObjectType.define do
|
|
||||||
em = ExposableModel.new(
|
|
||||||
model_class,
|
|
||||||
filter_strategy: type_builder.filter_strategy,
|
|
||||||
field_filter_list: type_builder.graphql_models[model_class][:fields],
|
|
||||||
assoc_filter_list: type_builder.graphql_models[model_class][:associations]
|
|
||||||
)
|
|
||||||
|
|
||||||
name(em.name)
|
|
||||||
description(em.description)
|
|
||||||
|
|
||||||
em.exposed_fields.each do |column|
|
|
||||||
ef = ExposableField.new(column)
|
|
||||||
field(ef.name, ef.graphql_type) # returns a GraphQL::Field
|
|
||||||
end
|
|
||||||
|
|
||||||
em.exposed_associations.each do |association|
|
|
||||||
ea = ExposableAssociation.new(association)
|
|
||||||
if ea.type.in? [:has_one, :belongs_to]
|
|
||||||
field(ea.name, -> { type_builder.graphql_types[association.klass] })
|
|
||||||
elsif ea.type.in? [:has_many]
|
|
||||||
field(ea.name, -> {
|
|
||||||
types[type_builder.graphql_types[association.klass]]
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return graphql_type
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
graphql_models = {}
|
|
||||||
|
|
||||||
graphql_models.store(User, {
|
|
||||||
fields: ['id', 'username'],
|
|
||||||
associations: ['proposals', 'debates']
|
|
||||||
})
|
|
||||||
graphql_models.store(Proposal, {
|
|
||||||
fields: ['id', 'title', 'description', 'author_id', 'created_at'],
|
|
||||||
associations: ['author']
|
|
||||||
})
|
|
||||||
graphql_models.store(Debate, {
|
|
||||||
fields: ['id', 'title', 'description', 'author_id', 'created_at'],
|
|
||||||
associations: ['author']
|
|
||||||
})
|
|
||||||
graphql_models.store(Comment, {
|
|
||||||
fields: ['id', 'commentable_id', 'commentable_type', 'body'],
|
|
||||||
associations: ['author']
|
|
||||||
})
|
|
||||||
graphql_models.store(Geozone, {
|
|
||||||
fields: ['id', 'name', 'html_map_coordinates'],
|
|
||||||
associations: []
|
|
||||||
})
|
|
||||||
|
|
||||||
TYPE_BUILDER = TypeBuilder.new(graphql_models, filter_strategy: :whitelist)
|
|
||||||
@@ -44,9 +44,6 @@ module Consul
|
|||||||
config.autoload_paths << "#{Rails.root}/app/controllers/custom"
|
config.autoload_paths << "#{Rails.root}/app/controllers/custom"
|
||||||
config.autoload_paths << "#{Rails.root}/app/models/custom"
|
config.autoload_paths << "#{Rails.root}/app/models/custom"
|
||||||
config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom'))
|
config.paths['app/views'].unshift(Rails.root.join('app', 'views', 'custom'))
|
||||||
|
|
||||||
# Add GraphQL directories to the autoload path
|
|
||||||
config.autoload_paths << Rails.root.join('app', 'graph')
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
46
config/initializers/graphql.rb
Normal file
46
config/initializers/graphql.rb
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
API_TYPE_DEFINITIONS = {
|
||||||
|
User => %I[ id username ],
|
||||||
|
Proposal => %I[ id title description author_id author created_at ]
|
||||||
|
}
|
||||||
|
|
||||||
|
api_types = {}
|
||||||
|
|
||||||
|
API_TYPE_DEFINITIONS.each do |model, fields|
|
||||||
|
api_types[model] = GraphQL::TypeCreator.create(model, fields, api_types)
|
||||||
|
end
|
||||||
|
|
||||||
|
ConsulSchema = GraphQL::Schema.define do
|
||||||
|
query QueryRoot
|
||||||
|
|
||||||
|
# Reject deeply-nested queries
|
||||||
|
max_depth 7
|
||||||
|
|
||||||
|
resolve_type -> (object, ctx) {
|
||||||
|
# look up types by class name
|
||||||
|
type_name = object.class.name
|
||||||
|
ConsulSchema.types[type_name]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
QueryRoot = GraphQL::ObjectType.define do
|
||||||
|
name "Query"
|
||||||
|
description "The query root for this schema"
|
||||||
|
|
||||||
|
field :proposal do
|
||||||
|
type api_types[Proposal]
|
||||||
|
description "Find a Proposal by id"
|
||||||
|
argument :id, !types.ID
|
||||||
|
resolve -> (object, arguments, context) {
|
||||||
|
Proposal.find(arguments["id"])
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
field :proposals do
|
||||||
|
type types[api_types[Proposal]]
|
||||||
|
description "Find all Proposals"
|
||||||
|
resolve -> (object, arguments, context) {
|
||||||
|
Proposal.all
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
42
lib/graph_ql/type_creator.rb
Normal file
42
lib/graph_ql/type_creator.rb
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
module GraphQL
|
||||||
|
class TypeCreator
|
||||||
|
|
||||||
|
# Return a GraphQL type for a 'database_type'
|
||||||
|
TYPES_CONVERSION = Hash.new(GraphQL::STRING_TYPE).merge(
|
||||||
|
integer: GraphQL::INT_TYPE,
|
||||||
|
boolean: GraphQL::BOOLEAN_TYPE,
|
||||||
|
float: GraphQL::FLOAT_TYPE,
|
||||||
|
double: GraphQL::FLOAT_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
def self.create(model, field_names, api_types)
|
||||||
|
|
||||||
|
new_graphql_type = GraphQL::ObjectType.define do
|
||||||
|
|
||||||
|
name(model.name)
|
||||||
|
description("Generated programmatically from model: #{model.name}")
|
||||||
|
|
||||||
|
# Make a field for each column
|
||||||
|
field_names.each do |field_name|
|
||||||
|
if model.column_names.include?(field_name.to_s)
|
||||||
|
field(field_name.to_s, TYPES_CONVERSION[field_name])
|
||||||
|
else
|
||||||
|
association = model.reflect_on_all_associations.find { |a| a.name == field_name }
|
||||||
|
case association.macro
|
||||||
|
when :has_one
|
||||||
|
field(association.name, -> { api_types[association.klass] })
|
||||||
|
when :belongs_to
|
||||||
|
field(association.name, -> { api_types[association.klass] })
|
||||||
|
when :has_many
|
||||||
|
connection(association.name, api_types[association.klass].connection_type {
|
||||||
|
description "Description for a field with pagination"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return new_graphql_type
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user