graphql_rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.hound.yml +3 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +34 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +5 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +20 -0
  10. data/Gemfile.lock +98 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +122 -0
  13. data/Rakefile +6 -0
  14. data/bin/console +14 -0
  15. data/bin/setup +8 -0
  16. data/graphql_rails.gemspec +29 -0
  17. data/lib/graphiti.rb +10 -0
  18. data/lib/graphiti/attribute.rb +86 -0
  19. data/lib/graphiti/controller.rb +54 -0
  20. data/lib/graphiti/controller/action_configuration.rb +46 -0
  21. data/lib/graphiti/controller/configuration.rb +32 -0
  22. data/lib/graphiti/controller/controller_function.rb +41 -0
  23. data/lib/graphiti/controller/request.rb +40 -0
  24. data/lib/graphiti/errors/execution_error.rb +18 -0
  25. data/lib/graphiti/errors/validation_error.rb +26 -0
  26. data/lib/graphiti/model.rb +33 -0
  27. data/lib/graphiti/model/configuration.rb +81 -0
  28. data/lib/graphiti/router.rb +65 -0
  29. data/lib/graphiti/router/action.rb +21 -0
  30. data/lib/graphiti/router/mutation_action.rb +18 -0
  31. data/lib/graphiti/router/query_action.rb +18 -0
  32. data/lib/graphiti/router/resource_actions_builder.rb +82 -0
  33. data/lib/graphiti/router/schema_builder.rb +37 -0
  34. data/lib/graphiti/version.rb +5 -0
  35. data/lib/graphql_rails.rb +10 -0
  36. data/lib/graphql_rails/attribute.rb +86 -0
  37. data/lib/graphql_rails/controller.rb +54 -0
  38. data/lib/graphql_rails/controller/action_configuration.rb +46 -0
  39. data/lib/graphql_rails/controller/action_path_parser.rb +75 -0
  40. data/lib/graphql_rails/controller/configuration.rb +32 -0
  41. data/lib/graphql_rails/controller/controller_function.rb +41 -0
  42. data/lib/graphql_rails/controller/request.rb +40 -0
  43. data/lib/graphql_rails/errors/execution_error.rb +18 -0
  44. data/lib/graphql_rails/errors/validation_error.rb +26 -0
  45. data/lib/graphql_rails/model.rb +33 -0
  46. data/lib/graphql_rails/model/configuration.rb +81 -0
  47. data/lib/graphql_rails/router.rb +65 -0
  48. data/lib/graphql_rails/router/action.rb +21 -0
  49. data/lib/graphql_rails/router/mutation_action.rb +18 -0
  50. data/lib/graphql_rails/router/query_action.rb +18 -0
  51. data/lib/graphql_rails/router/resource_actions_builder.rb +82 -0
  52. data/lib/graphql_rails/router/schema_builder.rb +37 -0
  53. data/lib/graphql_rails/version.rb +5 -0
  54. metadata +166 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+ require_relative 'request'
5
+
6
+ module GraphqlRails
7
+ class Controller
8
+ # graphql resolver which redirects actions to appropriate controller and controller action
9
+ class ActionPathParser
10
+ # accepts path of given format "controller_name#action"
11
+ def initialize(action_path, **options)
12
+ @action_path = action_path
13
+ @module_name = options[:module] || ''
14
+ end
15
+
16
+ def return_type
17
+ return_type = action.return_type || default_type
18
+
19
+ if action.can_return_nil?
20
+ return_type
21
+ else
22
+ return_type.to_non_null_type
23
+ end
24
+ end
25
+
26
+ def arguments
27
+ action.attributes.values
28
+ end
29
+
30
+ def controller
31
+ @controller ||= "#{namespaced_controller_name}_controller".classify.constantize
32
+ end
33
+
34
+ def action_name
35
+ @action_name ||= action_path.split('#').last
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :action_path, :module_name
41
+
42
+ def action
43
+ controller.controller_configuration.action(action_name)
44
+ end
45
+
46
+ def namespaced_controller_name
47
+ [module_name, controller_name].reject(&:empty?).join('/')
48
+ end
49
+
50
+ def controller_name
51
+ @controller_name ||= action_path.split('#').first
52
+ end
53
+
54
+ def action_model
55
+ model_path = namespaced_controller_name.singularize.classify.split('::')
56
+ model_name = model_path.pop
57
+
58
+ while model_path.any?
59
+ begin
60
+ return [model_path, model_name].join('::').constantize
61
+ rescue NameError => err
62
+ raise unless err.message.match?(/uninitialized constant/)
63
+ model_path.pop
64
+ end
65
+ end
66
+
67
+ model_name.constantize
68
+ end
69
+
70
+ def default_type
71
+ action_model.graphql.graphql_type
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'graphql_rails/attribute'
5
+ require 'graphql_rails/controller/action_configuration'
6
+
7
+ module GraphqlRails
8
+ class Controller
9
+ # stores all graphql_rails contoller specific config
10
+ class Configuration
11
+ attr_reader :before_actions
12
+
13
+ def initialize(controller)
14
+ @controller = controller
15
+ @before_actions = Set.new
16
+ @action_by_name = {}
17
+ end
18
+
19
+ def add_before_action(name)
20
+ before_actions << name
21
+ end
22
+
23
+ def action(method_name)
24
+ @action_by_name[method_name.to_s] ||= ActionConfiguration.new
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :controller
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/controller/action_path_parser'
4
+ require_relative 'request'
5
+
6
+ module GraphqlRails
7
+ class Controller
8
+ # graphql resolver which redirects actions to appropriate controller and controller action
9
+ class ControllerFunction < GraphQL::Function
10
+ # accepts path of given format "controller_name#action"
11
+ attr_reader :type
12
+
13
+ def initialize(controller, action_name, return_type)
14
+ @controller = controller
15
+ @action_name = action_name
16
+ @type = return_type
17
+ end
18
+
19
+ def self.build(action_path, **options)
20
+ action_parser = ActionPathParser.new(action_path, **options)
21
+
22
+ action_function = Class.new(self) do
23
+ action_parser.arguments.each do |action_attribute|
24
+ argument(action_attribute.field_name, action_attribute.graphql_field_type)
25
+ end
26
+ end
27
+
28
+ action_function.new(action_parser.controller, action_parser.action_name, action_parser.return_type)
29
+ end
30
+
31
+ def call(object, inputs, ctx)
32
+ request = Request.new(object, inputs, ctx)
33
+ controller.new(request).call(action_name)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :controller, :action_name
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors/execution_error'
4
+
5
+ module GraphqlRails
6
+ class Controller
7
+ # Contains all info related with single request to controller
8
+ class Request
9
+ attr_accessor :object_to_return
10
+ attr_reader :errors
11
+
12
+ def initialize(graphql_object, inputs, context)
13
+ @graphql_object = graphql_object
14
+ @inputs = inputs
15
+ @context = context
16
+ end
17
+
18
+ def errors=(new_errors)
19
+ @errors = new_errors
20
+
21
+ new_errors.each do |error|
22
+ error_message = error.is_a?(String) ? error : error.message
23
+ context.add_error(ExecutionError.new(error_message))
24
+ end
25
+ end
26
+
27
+ def no_object_to_return?
28
+ !defined?(@object_to_return)
29
+ end
30
+
31
+ def params
32
+ inputs.to_h
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :graphql_object, :inputs, :context
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # base class which is returned in case something bad happens. Contains all error rendering tructure
5
+ class ExecutionError < GraphQL::ExecutionError
6
+ def to_h
7
+ super.except('locations').merge('type' => type, 'http_status_code' => http_status_code)
8
+ end
9
+
10
+ def type
11
+ 'system_error'
12
+ end
13
+
14
+ def http_status_code
15
+ 500
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # GrapqhQL error that is raised when invalid data is given
5
+ class ValidationError < ExecutionError
6
+ attr_reader :short_message, :field
7
+
8
+ def initialize(short_message, field)
9
+ super([field.presence, short_message].compact.join(' '))
10
+ @short_message = short_message
11
+ @field = field
12
+ end
13
+
14
+ def type
15
+ 'validation_error'
16
+ end
17
+
18
+ def http_status_code
19
+ 422
20
+ end
21
+
22
+ def to_h
23
+ super.merge('field' => field, 'short_message' => short_message)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model/configuration'
4
+
5
+ module GraphqlRails
6
+ # this module allows to convert any ruby class in to grapql type object
7
+ #
8
+ # usage:
9
+ # class YourModel
10
+ # include GraphqlRails::Model
11
+ #
12
+ # graphql do
13
+ # attribute :id
14
+ # attribute :title
15
+ # end
16
+ # end
17
+ #
18
+ # YourModel.new.grapql_type # => type with [:id, :title] attributes
19
+ module Model
20
+ def self.included(base)
21
+ base.extend(ClassMethods)
22
+ end
23
+
24
+ # static methods for GraphqlRails::Model
25
+ module ClassMethods
26
+ def graphql
27
+ @graphql ||= Model::Configuration.new(self)
28
+ yield(@graphql) if block_given?
29
+ @graphql
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/attribute'
4
+
5
+ module GraphqlRails
6
+ module Model
7
+ # stores information about model specific config, like attributes and types
8
+ class Configuration
9
+ attr_reader :attributes
10
+
11
+ def initialize(model_class)
12
+ @model_class = model_class
13
+ @attributes = {}
14
+ end
15
+
16
+ def attribute(attribute_name, type: nil, hidden: false)
17
+ attributes[attribute_name.to_s] = Attribute.new(attribute_name, type, hidden: hidden)
18
+ end
19
+
20
+ def include_model_attributes(except: [])
21
+ except = Array(except).map(&:to_s)
22
+
23
+ if defined?(Mongoid) && model_class < Mongoid::Document
24
+ assign_default_mongoid_attributes(except: except)
25
+ elsif defined?(ActiveRecord) && model_class < ActiveRecord::Base
26
+ assign_default_active_record_attributes(except: except)
27
+ end
28
+ end
29
+
30
+ def graphql_type
31
+ @graphql_type ||= generate_graphql_type(graphql_type_name, visible_attributes)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :model_class
37
+
38
+ def visible_attributes
39
+ attributes.reject { |_name, attribute| attribute.hidden? }
40
+ end
41
+
42
+ def graphql_type_name
43
+ model_class.name.split('::').last
44
+ end
45
+
46
+ def generate_graphql_type(type_name, attributes)
47
+ GraphQL::ObjectType.define do
48
+ name(type_name)
49
+ description("Generated programmatically from model: #{type_name}")
50
+
51
+ attributes.each_value do |attribute|
52
+ field(attribute.field_name, attribute.graphql_field_type, property: attribute.name.to_sym)
53
+ end
54
+ end
55
+ end
56
+
57
+ def assign_default_mongoid_attributes(except: [])
58
+ allowed_fields = model_class.fields.except('_type', '_id', *except)
59
+
60
+ attribute('id', type: 'id')
61
+
62
+ allowed_fields.each_value do |field|
63
+ attribute(field.name, type: field.type.to_s.split('::').last)
64
+ end
65
+ end
66
+
67
+ def assign_default_active_record_attributes(except: [])
68
+ allowed_fields = model_class.columns.index_by(&:name).except('type', *except)
69
+
70
+ allowed_fields.each_value do |field|
71
+ field_type = field.cast_type.class.to_s.downcase.split('::').last
72
+ field_type = 'string' if field_type.ends_with?('string')
73
+ field_type = 'date' if field_type.include?('date')
74
+ field_type = 'time' if field_type.include?('time')
75
+
76
+ attribute(field.name, type: field_type.to_s.split('::').last)
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/inflections'
4
+
5
+ require_relative 'router/schema_builder'
6
+ require_relative 'router/mutation_action'
7
+ require_relative 'router/query_action'
8
+ require_relative 'router/resource_actions_builder'
9
+
10
+ module GraphqlRails
11
+ # graphql router that mimics Rails.application.routes
12
+ class Router
13
+ def self.draw(&block)
14
+ router = new
15
+ router.instance_eval(&block)
16
+ router.graphql_schema
17
+ end
18
+
19
+ attr_reader :actions, :namespace_name
20
+
21
+ def initialize(module_name: '')
22
+ @module_name = module_name
23
+ @actions ||= Set.new
24
+ end
25
+
26
+ def scope(**options, &block)
27
+ full_module_name = [module_name, options[:module]].reject(&:empty?).join('/')
28
+ scoped_router = self.class.new(module_name: full_module_name)
29
+ scoped_router.instance_eval(&block)
30
+ actions.merge(scoped_router.actions)
31
+ end
32
+
33
+ def resources(name, **options, &block)
34
+ builder_options = default_action_options.merge(options)
35
+ actions_builder = ResourceActionsBuilder.new(name, **builder_options)
36
+ actions_builder.instance_eval(&block) if block
37
+ actions.merge(actions_builder.actions)
38
+ end
39
+
40
+ def query(name, **options)
41
+ actions << build_action(QueryAction, name, **options)
42
+ end
43
+
44
+ def mutation(name, **options)
45
+ actions << build_action(MutationAction, name, **options)
46
+ end
47
+
48
+ def graphql_schema
49
+ SchemaBuilder.new(queries: actions.select(&:query?), mutations: actions.select(&:mutation?)).call
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :module_name
55
+
56
+ def build_action(action_builder, name, **options)
57
+ action_options = default_action_options.merge(options)
58
+ action_builder.new(name, action_options)
59
+ end
60
+
61
+ def default_action_options
62
+ { module: module_name }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../controller/controller_function'
4
+
5
+ module GraphqlRails
6
+ class Router
7
+ # Generic class for any type graphql action. Should not be used directly
8
+ class Action
9
+ attr_reader :name, :controller_action_path
10
+
11
+ def initialize(name, to:, **options)
12
+ @name = name.to_s.camelize(:lower)
13
+ @controller_action_path = [options[:module].to_s, to].reject(&:empty?).join('/')
14
+ end
15
+
16
+ def options
17
+ { function: Controller::ControllerFunction.build(controller_action_path) }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'action'
4
+
5
+ module GraphqlRails
6
+ class Router
7
+ # stores mutation type graphql action info
8
+ class MutationAction < Action
9
+ def query?
10
+ false
11
+ end
12
+
13
+ def mutation?
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end