graphql_rails 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/Gemfile.lock +20 -23
  4. data/docs/README.md +21 -5
  5. data/docs/_sidebar.md +3 -0
  6. data/docs/components/controller.md +174 -17
  7. data/docs/components/model.md +151 -3
  8. data/docs/components/routes.md +28 -0
  9. data/docs/getting_started/quick_start.md +9 -2
  10. data/docs/other_tools/query_runner.md +49 -0
  11. data/docs/other_tools/schema_dump.md +29 -0
  12. data/docs/testing/testing.md +3 -1
  13. data/graphql_rails.gemspec +1 -1
  14. data/lib/generators/graphql_rails/install_generator.rb +50 -0
  15. data/lib/generators/graphql_rails/templates/example_users_controller.erb +19 -0
  16. data/lib/generators/graphql_rails/templates/graphql_application_controller.erb +8 -0
  17. data/lib/generators/graphql_rails/templates/graphql_controller.erb +20 -0
  18. data/lib/generators/graphql_rails/templates/graphql_router.erb +19 -0
  19. data/lib/generators/graphql_rails/templates/graphql_router_spec.erb +18 -0
  20. data/lib/graphql_rails.rb +2 -0
  21. data/lib/graphql_rails/attributes/attributable.rb +16 -13
  22. data/lib/graphql_rails/attributes/attribute.rb +20 -3
  23. data/lib/graphql_rails/attributes/input_attribute.rb +20 -10
  24. data/lib/graphql_rails/attributes/input_type_parser.rb +24 -46
  25. data/lib/graphql_rails/attributes/type_parseable.rb +128 -0
  26. data/lib/graphql_rails/attributes/type_parser.rb +58 -54
  27. data/lib/graphql_rails/concerns/service.rb +15 -0
  28. data/lib/graphql_rails/controller.rb +20 -16
  29. data/lib/graphql_rails/controller/action.rb +10 -69
  30. data/lib/graphql_rails/controller/action_configuration.rb +70 -34
  31. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +52 -0
  32. data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb +28 -0
  33. data/lib/graphql_rails/controller/configuration.rb +56 -5
  34. data/lib/graphql_rails/controller/log_controller_action.rb +4 -4
  35. data/lib/graphql_rails/controller/request.rb +29 -8
  36. data/lib/graphql_rails/controller/request/format_errors.rb +58 -0
  37. data/lib/graphql_rails/decorator/relation_decorator.rb +1 -1
  38. data/lib/graphql_rails/errors/custom_execution_error.rb +22 -0
  39. data/lib/graphql_rails/errors/execution_error.rb +6 -7
  40. data/lib/graphql_rails/errors/system_error.rb +14 -0
  41. data/lib/graphql_rails/errors/validation_error.rb +1 -5
  42. data/lib/graphql_rails/input_configurable.rb +47 -0
  43. data/lib/graphql_rails/model.rb +19 -4
  44. data/lib/graphql_rails/model/build_connection_type.rb +48 -0
  45. data/lib/graphql_rails/model/{configuration → build_connection_type}/count_items.rb +4 -4
  46. data/lib/graphql_rails/model/build_enum_type.rb +39 -10
  47. data/lib/graphql_rails/model/build_graphql_input_type.rb +7 -3
  48. data/lib/graphql_rails/model/build_graphql_type.rb +23 -7
  49. data/lib/graphql_rails/model/call_graphql_model_method.rb +59 -0
  50. data/lib/graphql_rails/model/configuration.rb +2 -6
  51. data/lib/graphql_rails/model/input.rb +10 -6
  52. data/lib/graphql_rails/query_runner.rb +68 -0
  53. data/lib/graphql_rails/railtie.rb +10 -0
  54. data/lib/graphql_rails/router.rb +40 -13
  55. data/lib/graphql_rails/router/resource_routes_builder.rb +2 -1
  56. data/lib/graphql_rails/router/route.rb +25 -6
  57. data/lib/graphql_rails/router/schema_builder.rb +26 -11
  58. data/lib/graphql_rails/rspec_controller_helpers.rb +4 -2
  59. data/lib/graphql_rails/tasks/dump_graphql_schema.rb +57 -0
  60. data/lib/graphql_rails/tasks/schema.rake +14 -0
  61. data/lib/graphql_rails/version.rb +1 -1
  62. metadata +29 -9
  63. data/lib/graphql_rails/controller/controller_function.rb +0 -50
  64. data/lib/graphql_rails/controller/format_results.rb +0 -36
@@ -9,14 +9,14 @@ module GraphqlRails
9
9
  class Controller
10
10
  # analyzes route and extracts controller action related data
11
11
  class Action
12
- class MissingConfigurationError < GraphqlRails::Error; end
12
+ delegate :relative_path, to: :route
13
13
 
14
14
  def initialize(route)
15
15
  @route = route
16
16
  end
17
17
 
18
18
  def return_type
19
- action_config.return_type || default_type
19
+ action_config.return_type
20
20
  end
21
21
 
22
22
  def arguments
@@ -28,56 +28,25 @@ module GraphqlRails
28
28
  end
29
29
 
30
30
  def name
31
- @name ||= action_relative_path.split('#').last
31
+ @name ||= relative_path.split('#').last
32
32
  end
33
33
 
34
34
  def description
35
35
  action_config.description
36
36
  end
37
37
 
38
- private
39
-
40
- attr_reader :route
41
-
42
- def default_inner_return_type
43
- raise_missing_return_type_error if model_graphql_type.nil?
44
-
45
- if action_config.can_return_nil?
46
- model_graphql_type
47
- else
48
- model_graphql_type.to_non_null_type
49
- end
50
- end
51
-
52
- def raise_missing_return_type_error
53
- error_message = \
54
- "Return type for #{route.path.inspect} is not defined. " \
55
- "To do so, add `action(:#{name}).returns(YourType)` in #{controller.name} " \
56
- "or make sure that you have model named #{namespaced_model_name}"
57
-
58
- raise MissingConfigurationError, error_message
38
+ def type_args
39
+ [type_parser.type_arg, null: !type_parser.required?]
59
40
  end
60
41
 
61
- def default_type
62
- return default_collection_type if route.collection?
63
-
64
- default_inner_return_type
65
- end
42
+ private
66
43
 
67
- def default_collection_type
68
- if action_config.paginated?
69
- action_model.graphql.connection_type
70
- else
71
- default_inner_return_type.to_list_type.to_non_null_type
72
- end
73
- end
44
+ attr_reader :route
74
45
 
75
- def action_relative_path
76
- route.relative_path
77
- end
46
+ delegate :type_parser, to: :action_config
78
47
 
79
48
  def action_config
80
- controller.controller_configuration.action(name)
49
+ controller.controller_configuration.action_config(name)
81
50
  end
82
51
 
83
52
  def namespaced_controller_name
@@ -85,40 +54,12 @@ module GraphqlRails
85
54
  end
86
55
 
87
56
  def controller_name
88
- @controller_name ||= action_relative_path.split('#').first
89
- end
90
-
91
- def action_model
92
- namespace = namespaced_model_name.split('::')
93
- model_name = namespace.pop
94
- model = nil
95
-
96
- while model.nil? && !namespace.empty?
97
- model = namespaced_model(namespace, model_name)
98
- namespace.pop
99
- end
100
-
101
- model || namespaced_model(namespace, model_name)
102
- end
103
-
104
- def namespaced_model(namespace, model_name)
105
- [namespace, model_name].join('::').constantize
106
- rescue NameError => err
107
- raise unless err.message.match?(/uninitialized constant/)
108
-
109
- nil
57
+ @controller_name ||= relative_path.split('#').first
110
58
  end
111
59
 
112
60
  def namespaced_model_name
113
61
  namespaced_controller_name.singularize.classify
114
62
  end
115
-
116
- def model_graphql_type
117
- return unless action_model
118
- return unless action_model < GraphqlRails::Model
119
-
120
- action_model.graphql.graphql_type
121
- end
122
63
  end
123
64
  end
124
65
  end
@@ -2,50 +2,54 @@
2
2
 
3
3
  require 'active_support/core_ext/string/filters'
4
4
  require 'graphql_rails/attributes'
5
+ require 'graphql_rails/input_configurable'
6
+ require 'graphql_rails/errors/error'
5
7
 
6
8
  module GraphqlRails
7
9
  class Controller
8
10
  # stores all graphql_rails contoller specific config
9
11
  class ActionConfiguration
10
- attr_reader :attributes, :pagination_options
12
+ class MissingConfigurationError < GraphqlRails::Error; end
13
+ class DeprecatedDefaultModelError < GraphqlRails::Error; end
14
+
15
+ include InputConfigurable
16
+
17
+ attr_reader :attributes, :pagination_options, :name, :controller, :defined_at
11
18
 
12
19
  def initialize_copy(other)
13
20
  super
14
21
  @attributes = other.instance_variable_get(:@attributes).dup.transform_values(&:dup)
22
+ @action_options = other.instance_variable_get(:@action_options).dup.transform_values(&:dup)
23
+ @pagination_options = other.instance_variable_get(:@pagination_options)&.dup&.transform_values(&:dup)
15
24
  end
16
25
 
17
- def initialize
26
+ def initialize(name:, controller:)
27
+ @name = name
28
+ @controller = controller
18
29
  @attributes = {}
19
30
  @action_options = {}
20
- @can_return_nil = false
21
31
  end
22
32
 
23
- def options(input_format:)
24
- @action_options[:input_format] = input_format
25
- self
33
+ def dup_with(name:, controller:, defined_at:)
34
+ dup.tap do |new_action|
35
+ new_action.instance_variable_set(:@defined_at, defined_at)
36
+ new_action.instance_variable_set(:@name, name)
37
+ new_action.instance_variable_set(:@controller, controller)
38
+ end
26
39
  end
27
40
 
28
- def permit(*no_type_attributes, **typed_attributes)
29
- no_type_attributes.each { |attribute| permit_input(attribute) }
30
- typed_attributes.each { |attribute, type| permit_input(attribute, type: type) }
31
- self
32
- end
41
+ def options(action_options = nil)
42
+ @input_attribute_options ||= {}
43
+ return @input_attribute_options if action_options.nil?
33
44
 
34
- def permit_input(name, type: nil, options: {}, **input_options)
35
- field_name = name.to_s.remove(/!\Z/)
45
+ @input_attribute_options[:input_format] = action_options[:input_format] if action_options[:input_format]
36
46
 
37
- attributes[field_name] = Attributes::InputAttribute.new(
38
- name.to_s, type,
39
- options: action_options.merge(options),
40
- **input_options
41
- )
42
47
  self
43
48
  end
44
49
 
45
- def paginated(pagination_options = {})
50
+ def paginated(*args)
46
51
  @return_type = nil
47
- @pagination_options = pagination_options
48
- permit(:before, :after, first: :int, last: :int)
52
+ super
49
53
  end
50
54
 
51
55
  def description(new_description = nil)
@@ -57,35 +61,51 @@ module GraphqlRails
57
61
  end
58
62
  end
59
63
 
60
- def can_return_nil
61
- @can_return_nil = true
62
- self
63
- end
64
-
65
64
  def returns(custom_return_type)
66
65
  @return_type = nil
67
66
  @custom_return_type = custom_return_type
68
67
  self
69
68
  end
70
69
 
71
- def can_return_nil?
72
- @can_return_nil
70
+ def model(model_name = nil)
71
+ if model_name
72
+ @model = model_name
73
+ self
74
+ else
75
+ @model || raise_missing_config_error
76
+ end
73
77
  end
74
78
 
75
- def paginated?
76
- !!pagination_options # rubocop:disable Style/DoubleNegation
79
+ def returns_single(required: true)
80
+ model_name = model.to_s
81
+ model_name = "#{model_name}!" if required
82
+
83
+ returns(model_name)
84
+ end
85
+
86
+ def returns_list(required_inner: true, required_list: true)
87
+ model_name = model.to_s
88
+ model_name = "#{model_name}!" if required_inner
89
+ list_name = "[#{model_name}]"
90
+ list_name = "#{list_name}!" if required_list
91
+
92
+ returns(list_name)
77
93
  end
78
94
 
79
95
  def return_type
80
96
  @return_type ||= build_return_type
81
97
  end
82
98
 
99
+ def type_parser
100
+ @type_parser ||= Attributes::TypeParser.new(custom_return_type, paginated: paginated?)
101
+ end
102
+
83
103
  private
84
104
 
85
- attr_reader :custom_return_type, :action_options
105
+ attr_reader :custom_return_type
86
106
 
87
107
  def build_return_type
88
- return nil if custom_return_type.nil?
108
+ return raise_deprecation_error if custom_return_type.nil?
89
109
 
90
110
  if paginated?
91
111
  type_parser.graphql_model ? type_parser.graphql_model.graphql.connection_type : nil
@@ -94,8 +114,24 @@ module GraphqlRails
94
114
  end
95
115
  end
96
116
 
97
- def type_parser
98
- Attributes::TypeParser.new(custom_return_type)
117
+ def raise_deprecation_error
118
+ message = \
119
+ 'Default return types are deprecated. ' \
120
+ "You need to manually set something like `action(:#{name}).returns('#{suggested_model_name}')`"
121
+
122
+ full_backtrace = ([defined_at] + caller).compact
123
+ raise DeprecatedDefaultModelError, message, full_backtrace
124
+ end
125
+
126
+ def suggested_model_name
127
+ controller&.name.to_s.demodulize.sub(/Controller$/, '').singularize
128
+ end
129
+
130
+ def raise_missing_config_error
131
+ error_message = \
132
+ 'Default model for controller is not defined. To do so add `model(YourModel)`'
133
+
134
+ raise MissingConfigurationError, error_message
99
135
  end
100
136
  end
101
137
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/controller/action'
4
+ require 'graphql_rails/concerns/service'
5
+ require 'graphql_rails/controller/action_configuration'
6
+ require 'graphql_rails/controller/build_controller_action_resolver/controller_action_resolver'
7
+
8
+ module GraphqlRails
9
+ class Controller
10
+ # graphql resolver which redirects actions to appropriate controller and controller action
11
+ class BuildControllerActionResolver
12
+ include ::GraphqlRails::Service
13
+
14
+ def initialize(route:)
15
+ @route = route
16
+ end
17
+
18
+ def call # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
19
+ action = build_action
20
+
21
+ Class.new(ControllerActionResolver) do
22
+ type(*action.type_args)
23
+ description(action.description)
24
+ controller(action.controller)
25
+ controller_action_name(action.name)
26
+
27
+ action.arguments.each do |attribute|
28
+ argument(*attribute.input_argument_args)
29
+ end
30
+
31
+ def self.inspect
32
+ "ControllerActionResolver(#{controller.name}##{controller_action_name})"
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :route
40
+
41
+ def build_action
42
+ Action.new(route).tap do |action|
43
+ assert_action(action)
44
+ end
45
+ end
46
+
47
+ def assert_action(action)
48
+ action.return_type
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql_rails/controller/request'
4
+
5
+ module GraphqlRails
6
+ class Controller
7
+ class BuildControllerActionResolver
8
+ # Resolver which includes controller specific methods.
9
+ # Used to simplify resolver build for each controller action
10
+ class ControllerActionResolver < GraphQL::Schema::Resolver
11
+ def self.controller(controller_class = nil)
12
+ @controller = controller_class if controller_class
13
+ @controller
14
+ end
15
+
16
+ def self.controller_action_name(name = nil)
17
+ @controller_action_name = name if name
18
+ @controller_action_name
19
+ end
20
+
21
+ def resolve(**inputs)
22
+ request = Request.new(object, inputs, context)
23
+ self.class.controller.new(request).call(self.class.controller_action_name)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -3,14 +3,20 @@
3
3
  require 'active_support/core_ext/string/inflections'
4
4
  require 'graphql_rails/controller/action_configuration'
5
5
  require 'graphql_rails/controller/action_hook'
6
+ require 'graphql_rails/errors/error'
6
7
 
7
8
  module GraphqlRails
8
9
  class Controller
9
10
  # stores all graphql_rails contoller specific config
10
11
  class Configuration
12
+ class InvalidActionConfiguration < GraphqlRails::Error; end
13
+
14
+ LIB_REGEXP = %r{/graphql_rails/lib/}
15
+
11
16
  attr_reader :action_by_name
12
17
 
13
- def initialize
18
+ def initialize(controller)
19
+ @controller = controller
14
20
  @hooks = {
15
21
  before: {},
16
22
  after: {},
@@ -18,6 +24,7 @@ module GraphqlRails
18
24
  }
19
25
 
20
26
  @action_by_name = {}
27
+ @action_default = nil
21
28
  end
22
29
 
23
30
  def initialize_copy(other)
@@ -31,6 +38,12 @@ module GraphqlRails
31
38
  end
32
39
  end
33
40
 
41
+ def dup_with(controller:)
42
+ dup.tap do |new_config|
43
+ new_config.instance_variable_set(:@controller, controller)
44
+ end
45
+ end
46
+
34
47
  def action_hooks_for(hook_type, action_name)
35
48
  hooks[hook_type].values.select { |hook| hook.applicable_for?(action_name) }
36
49
  end
@@ -43,15 +56,53 @@ module GraphqlRails
43
56
  ActionHook.new(name: hook_name, **options, &block)
44
57
  end
45
58
 
59
+ def action_default
60
+ @action_default ||= ActionConfiguration.new(name: :default, controller: nil)
61
+ yield(@action_default) if block_given?
62
+ @action_default
63
+ end
64
+
46
65
  def action(method_name)
47
- @action_by_name[method_name.to_s] ||= ActionConfiguration.new
48
- yield(@action_by_name[method_name.to_s]) if block_given?
49
- @action_by_name[method_name.to_s]
66
+ action_name = method_name.to_s.underscore
67
+ @action_by_name[action_name] ||= action_default.dup_with(
68
+ name: action_name,
69
+ controller: controller,
70
+ defined_at: dynamic_source_location
71
+ )
72
+ yield(@action_by_name[action_name]) if block_given?
73
+ @action_by_name[action_name]
74
+ end
75
+
76
+ def action_config(method_name)
77
+ action_name = method_name.to_s.underscore
78
+ @action_by_name.fetch(action_name) { raise_invalid_config_error(action_name) }
79
+ end
80
+
81
+ def model(model = nil)
82
+ action_default.model(model)
50
83
  end
51
84
 
52
85
  private
53
86
 
54
- attr_reader :hooks
87
+ attr_reader :hooks, :controller
88
+
89
+ def dynamic_source_location
90
+ project_trace = \
91
+ caller
92
+ .dup
93
+ .drop_while { |path| !path.match?(LIB_REGEXP) }
94
+ .drop_while { |path| path.match?(LIB_REGEXP) }
95
+
96
+ project_trace.first
97
+ end
98
+
99
+ def raise_invalid_config_error(action_name)
100
+ error_message = \
101
+ "Missing action configuration for #{controller}##{action_name}. " \
102
+ "Please define it with `action(:#{action_name})`."
103
+
104
+ raise InvalidActionConfiguration, error_message
105
+ end
55
106
  end
56
107
  end
57
108
  end