From 59a355df1b17df6a8ca05776a961b2bd14514884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Miedes=20Garc=C3=A9s?= Date: Thu, 10 Nov 2016 20:53:15 +0100 Subject: [PATCH] First working version of new GraphQL::TypeCreator after major refactoring --- app/graph/consul_schema.rb | 12 ---- app/graph/exposable_association.rb | 8 --- app/graph/exposable_field.rb | 26 --------- app/graph/exposable_model.rb | 36 ------------ app/graph/query_root.rb | 65 ---------------------- app/graph/type_builder.rb | 89 ------------------------------ config/application.rb | 3 - config/initializers/graphql.rb | 46 +++++++++++++++ lib/graph_ql/type_creator.rb | 42 ++++++++++++++ 9 files changed, 88 insertions(+), 239 deletions(-) delete mode 100644 app/graph/consul_schema.rb delete mode 100644 app/graph/exposable_association.rb delete mode 100644 app/graph/exposable_field.rb delete mode 100644 app/graph/exposable_model.rb delete mode 100644 app/graph/query_root.rb delete mode 100644 app/graph/type_builder.rb create mode 100644 config/initializers/graphql.rb create mode 100644 lib/graph_ql/type_creator.rb diff --git a/app/graph/consul_schema.rb b/app/graph/consul_schema.rb deleted file mode 100644 index 47ef8a112..000000000 --- a/app/graph/consul_schema.rb +++ /dev/null @@ -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 diff --git a/app/graph/exposable_association.rb b/app/graph/exposable_association.rb deleted file mode 100644 index 78e650b0a..000000000 --- a/app/graph/exposable_association.rb +++ /dev/null @@ -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 diff --git a/app/graph/exposable_field.rb b/app/graph/exposable_field.rb deleted file mode 100644 index 2b54083a7..000000000 --- a/app/graph/exposable_field.rb +++ /dev/null @@ -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 diff --git a/app/graph/exposable_model.rb b/app/graph/exposable_model.rb deleted file mode 100644 index 22976c63d..000000000 --- a/app/graph/exposable_model.rb +++ /dev/null @@ -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 diff --git a/app/graph/query_root.rb b/app/graph/query_root.rb deleted file mode 100644 index 928dceb6a..000000000 --- a/app/graph/query_root.rb +++ /dev/null @@ -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 diff --git a/app/graph/type_builder.rb b/app/graph/type_builder.rb deleted file mode 100644 index 1277f8746..000000000 --- a/app/graph/type_builder.rb +++ /dev/null @@ -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) diff --git a/config/application.rb b/config/application.rb index 623afc4f1..a3d048fef 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,9 +44,6 @@ module Consul config.autoload_paths << "#{Rails.root}/app/controllers/custom" config.autoload_paths << "#{Rails.root}/app/models/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 diff --git a/config/initializers/graphql.rb b/config/initializers/graphql.rb new file mode 100644 index 000000000..7dec74d12 --- /dev/null +++ b/config/initializers/graphql.rb @@ -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 diff --git a/lib/graph_ql/type_creator.rb b/lib/graph_ql/type_creator.rb new file mode 100644 index 000000000..44a542ed2 --- /dev/null +++ b/lib/graph_ql/type_creator.rb @@ -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