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
@@ -4,9 +4,9 @@ module GraphqlRails
4
4
  module Model
5
5
  # stores information about model specific config, like attributes and types
6
6
  class BuildGraphqlInputType
7
- def self.call(*args)
8
- new(*args).call
9
- end
7
+ require 'graphql_rails/concerns/service'
8
+
9
+ include ::GraphqlRails::Service
10
10
 
11
11
  def initialize(name:, description: nil, attributes:)
12
12
  @name = name
@@ -26,6 +26,10 @@ module GraphqlRails
26
26
  type_attributes.each_value do |type_attribute|
27
27
  argument(*type_attribute.input_argument_args)
28
28
  end
29
+
30
+ def self.inspect
31
+ "#{GraphQL::Schema::InputObject}(#{graphql_name})"
32
+ end
29
33
  end
30
34
  end
31
35
 
@@ -4,9 +4,12 @@ module GraphqlRails
4
4
  module Model
5
5
  # stores information about model specific config, like attributes and types
6
6
  class BuildGraphqlType
7
- def self.call(*args)
8
- new(*args).call
9
- end
7
+ require 'graphql_rails/concerns/service'
8
+ require 'graphql_rails/model/call_graphql_model_method'
9
+
10
+ include ::GraphqlRails::Service
11
+
12
+ PAGINATION_KEYS = %i[before after first last].freeze
10
13
 
11
14
  def initialize(name:, description: nil, attributes:)
12
15
  @name = name
@@ -19,19 +22,32 @@ module GraphqlRails
19
22
  type_description = description
20
23
  type_attributes = attributes
21
24
 
22
- GraphQL::ObjectType.define do
23
- name(type_name)
25
+ Class.new(GraphQL::Schema::Object) do
26
+ graphql_name(type_name)
24
27
  description(type_description)
25
28
 
26
29
  type_attributes.each_value do |attribute|
27
- field(*attribute.field_args)
30
+ field(*attribute.field_args) do
31
+ attribute.attributes.values.each do |arg_attribute|
32
+ argument(*arg_attribute.input_argument_args)
33
+ end
34
+ end
35
+
36
+ define_method attribute.property do |**kwargs|
37
+ CallGraphqlModelMethod.call(
38
+ model: object,
39
+ attribute_config: attribute,
40
+ method_keyword_arguments: kwargs,
41
+ graphql_context: context
42
+ )
43
+ end
28
44
  end
29
45
  end
30
46
  end
31
47
 
32
48
  private
33
49
 
34
- attr_reader :model_configuration, :attributes, :name, :description
50
+ attr_reader :attributes, :name, :description
35
51
  end
36
52
  end
37
53
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Model
5
+ # Executes model method and adds additional meta data if needed
6
+ class CallGraphqlModelMethod
7
+ require 'graphql_rails/concerns/service'
8
+
9
+ include ::GraphqlRails::Service
10
+
11
+ PAGINATION_KEYS = %i[before after first last].freeze
12
+
13
+ def initialize(model:, method_keyword_arguments:, graphql_context:, attribute_config:)
14
+ @model = model
15
+ @method_keyword_arguments = method_keyword_arguments
16
+ @graphql_context = graphql_context
17
+ @attribute_config = attribute_config
18
+ end
19
+
20
+ def call
21
+ with_graphql_context do
22
+ run_method
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :model, :attribute_config, :graphql_context, :method_keyword_arguments
29
+
30
+ def run_method
31
+ if custom_keyword_arguments.empty?
32
+ model.send(method_name)
33
+ else
34
+ model.send(method_name, **custom_keyword_arguments)
35
+ end
36
+ end
37
+
38
+ def method_name
39
+ attribute_config.property
40
+ end
41
+
42
+ def paginated?
43
+ attribute_config.paginated?
44
+ end
45
+
46
+ def custom_keyword_arguments
47
+ return method_keyword_arguments unless paginated?
48
+
49
+ method_keyword_arguments.except(*PAGINATION_KEYS)
50
+ end
51
+
52
+ def with_graphql_context
53
+ return yield unless model.respond_to?(:with_graphql_context)
54
+
55
+ model.with_graphql_context(graphql_context) { yield }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -5,7 +5,7 @@ require 'graphql_rails/model/build_graphql_type'
5
5
  require 'graphql_rails/model/build_enum_type'
6
6
  require 'graphql_rails/model/input'
7
7
  require 'graphql_rails/model/configurable'
8
- require 'graphql_rails/model/configuration/count_items'
8
+ require 'graphql_rails/model/build_connection_type'
9
9
 
10
10
  module GraphqlRails
11
11
  module Model
@@ -60,11 +60,7 @@ module GraphqlRails
60
60
  end
61
61
 
62
62
  def connection_type
63
- @connection_type ||= begin
64
- graphql_type.define_connection do
65
- field :total, types.Int, resolve: CountItems
66
- end
67
- end
63
+ @connection_type ||= BuildConnectionType.call(graphql_type)
68
64
  end
69
65
 
70
66
  private
@@ -14,6 +14,11 @@ module GraphqlRails
14
14
  @input_name_suffix = input_name_suffix
15
15
  end
16
16
 
17
+ def initialize_copy(other)
18
+ super
19
+ @attributes = other.instance_variable_get(:@attributes)&.transform_values(&:dup)
20
+ end
21
+
17
22
  def graphql_input_type
18
23
  @graphql_input_type ||= BuildGraphqlInputType.call(
19
24
  name: name, description: description, attributes: attributes
@@ -21,12 +26,11 @@ module GraphqlRails
21
26
  end
22
27
 
23
28
  def attribute(attribute_name, type: nil, enum: nil, **attribute_options)
24
- attributes[attribute_name.to_s] = \
25
- GraphqlRails::Attributes::InputAttribute.new(
26
- attribute_name,
27
- attribute_type(attribute_name, type: type, enum: enum, **attribute_options),
28
- attribute_options
29
- )
29
+ input_type = attribute_type(attribute_name, type: type, enum: enum, **attribute_options)
30
+
31
+ attributes[attribute_name.to_s] = Attributes::InputAttribute.new(
32
+ attribute_name, type: input_type, **attribute_options
33
+ )
30
34
  end
31
35
 
32
36
  private
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # executes GraphQL queries and returns json
5
+ class QueryRunner
6
+ require 'graphql_rails/router'
7
+ require 'graphql_rails/concerns/service'
8
+
9
+ include ::GraphqlRails::Service
10
+
11
+ def initialize(group: nil, params:, schema: nil, router: nil, **schema_options)
12
+ @group = group
13
+ @graphql_schema = schema
14
+ @params = params
15
+ @schema_options = schema_options
16
+ @router = router
17
+ end
18
+
19
+ def call
20
+ graphql_schema.execute(
21
+ params[:query],
22
+ variables: variables,
23
+ operation_name: params[:operationName],
24
+ **schema_options
25
+ )
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :schema_options, :params, :group
31
+
32
+ def variables
33
+ ensure_hash(params[:variables])
34
+ end
35
+
36
+ def graphql_schema
37
+ @graphql_schema ||= router_schema
38
+ end
39
+
40
+ def router
41
+ @router ||= ::GraphqlRouter
42
+ end
43
+
44
+ def router_schema
45
+ router.graphql_schema(group)
46
+ end
47
+
48
+ def ensure_hash(ambiguous_param)
49
+ if ambiguous_param.blank?
50
+ {}
51
+ elsif ambiguous_param.is_a?(String)
52
+ ensure_hash(JSON.parse(ambiguous_param))
53
+ elsif kind_of_hash?(ambiguous_param)
54
+ ambiguous_param
55
+ else
56
+ raise ArgumentError, "Unexpected parameter: #{ambiguous_param.inspect}"
57
+ end
58
+ end
59
+
60
+ def kind_of_hash?(object)
61
+ return true if object.is_a?(Hash)
62
+
63
+ defined?(ActionController) &&
64
+ defined?(ActionController::Parameters) &&
65
+ object.is_a?(ActionController::Parameters)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ # Used to load rake tasks in RoR app
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ load 'graphql_rails/tasks/schema.rake'
8
+ end
9
+ end
10
+ end
@@ -11,32 +11,39 @@ module GraphqlRails
11
11
  # graphql router that mimics Rails.application.routes
12
12
  class Router
13
13
  RAW_ACTION_NAMES = %i[
14
- rescue_from query_analyzer instrument cursor_encoder default_max_page_size
14
+ use rescue_from query_analyzer instrument cursor_encoder default_max_page_size
15
15
  ].freeze
16
16
 
17
17
  def self.draw(&block)
18
- router = new
19
- router.instance_eval(&block)
20
- router.graphql_schema
18
+ new.tap do |router|
19
+ router.instance_eval(&block)
20
+ end
21
21
  end
22
22
 
23
23
  attr_reader :routes, :namespace_name, :raw_graphql_actions
24
24
 
25
- def initialize(module_name: '')
25
+ def initialize(module_name: '', group_names: [])
26
26
  @module_name = module_name
27
+ @group_names = group_names
27
28
  @routes ||= Set.new
28
29
  @raw_graphql_actions ||= []
29
30
  end
30
31
 
32
+ def group(*group_names, &block)
33
+ scoped_router = router_with(group_names: group_names)
34
+ scoped_router.instance_eval(&block)
35
+ routes.merge(scoped_router.routes)
36
+ end
37
+
31
38
  def scope(**options, &block)
32
39
  full_module_name = [module_name, options[:module]].reject(&:empty?).join('/')
33
- scoped_router = self.class.new(module_name: full_module_name)
40
+ scoped_router = router_with(module_name: full_module_name)
34
41
  scoped_router.instance_eval(&block)
35
42
  routes.merge(scoped_router.routes)
36
43
  end
37
44
 
38
45
  def resources(name, **options, &block)
39
- builder_options = default_route_options.merge(options)
46
+ builder_options = full_route_options(options)
40
47
  routes_builder = ResourceRoutesBuilder.new(name, **builder_options)
41
48
  routes_builder.instance_eval(&block) if block
42
49
  routes.merge(routes_builder.routes)
@@ -56,25 +63,45 @@ module GraphqlRails
56
63
  end
57
64
  end
58
65
 
59
- def graphql_schema
60
- SchemaBuilder.new(
66
+ def graphql_schema(group = nil)
67
+ @graphql_schema ||= {}
68
+ @graphql_schema[group&.to_sym] ||= SchemaBuilder.new(
61
69
  queries: routes.select(&:query?),
62
70
  mutations: routes.select(&:mutation?),
63
- raw_actions: raw_graphql_actions
71
+ raw_actions: raw_graphql_actions,
72
+ group: group
64
73
  ).call
65
74
  end
66
75
 
76
+ def reload_schema
77
+ @graphql_schema = nil
78
+ end
79
+
67
80
  private
68
81
 
69
- attr_reader :module_name
82
+ attr_reader :module_name, :group_names
83
+
84
+ def router_with(new_router_options = {})
85
+ default_options = { module_name: module_name, group_names: group_names }
86
+ full_options = default_options.merge(new_router_options)
87
+
88
+ self.class.new(full_options)
89
+ end
70
90
 
71
91
  def add_raw_action(name, *args, &block)
72
92
  raw_graphql_actions << { name: name, args: args, block: block }
73
93
  end
74
94
 
75
95
  def build_route(route_builder, name, **options)
76
- route_options = default_route_options.merge(options)
77
- route_builder.new(name, route_options)
96
+ route_builder.new(name, full_route_options(options))
97
+ end
98
+
99
+ def full_route_options(extra_options)
100
+ extra_groups = Array(extra_options[:group]) + Array(extra_options[:groups])
101
+ extra_options = extra_options.except(:group, :groups)
102
+ groups = (group_names + extra_groups).uniq
103
+
104
+ default_route_options.merge(extra_options).merge(groups: groups)
78
105
  end
79
106
 
80
107
  def default_route_options
@@ -70,8 +70,9 @@ module GraphqlRails
70
70
  end
71
71
 
72
72
  action_options = options.merge(custom_options).merge(on: on)
73
+ controller_method_name = action.to_s.underscore
73
74
  action_name = [prefix, resource_name(on), suffix_name].map(&:to_s).reject(&:empty?).join('_')
74
- builder.new(action_name, to: "#{name}##{action}", **action_options)
75
+ builder.new(action_name, to: "#{name}##{controller_method_name}", **action_options)
75
76
  end
76
77
  # rubocop:enable Metrics/ParameterLists
77
78
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../controller/controller_function'
3
+ require_relative '../controller/build_controller_action_resolver'
4
4
 
5
5
  module GraphqlRails
6
6
  class Router
@@ -8,10 +8,11 @@ module GraphqlRails
8
8
  class Route
9
9
  attr_reader :name, :module_name, :on, :relative_path
10
10
 
11
- def initialize(name, to: '', on:, **options)
11
+ def initialize(name, to: '', on:, groups: nil, **options)
12
12
  @name = name.to_s.camelize(:lower)
13
13
  @module_name = options[:module].to_s
14
14
  @function = options[:function]
15
+ @groups = groups
15
16
  @relative_path = to
16
17
  @on = on.to_sym
17
18
  end
@@ -26,12 +27,30 @@ module GraphqlRails
26
27
  on == :collection
27
28
  end
28
29
 
29
- def member?
30
- on == :member
30
+ def show_in_group?(group_name)
31
+ return true if groups.nil? || groups.empty?
32
+
33
+ groups.include?(group_name&.to_sym)
31
34
  end
32
35
 
33
- def function
34
- @function ||= Controller::ControllerFunction.from_route(self)
36
+ def field_args
37
+ options = {}
38
+
39
+ if function
40
+ options[:function] = function
41
+ else
42
+ options[:resolver] = resolver
43
+ end
44
+
45
+ [name, options]
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :function, :groups
51
+
52
+ def resolver
53
+ @resolver ||= Controller::BuildControllerActionResolver.call(route: self)
35
54
  end
36
55
  end
37
56
  end
@@ -8,34 +8,49 @@ module GraphqlRails
8
8
 
9
9
  attr_reader :queries, :mutations, :raw_actions
10
10
 
11
- def initialize(queries:, mutations:, raw_actions:)
11
+ def initialize(queries:, mutations:, raw_actions:, group: nil)
12
12
  @queries = queries
13
13
  @mutations = mutations
14
14
  @raw_actions = raw_actions
15
+ @group = group
15
16
  end
16
17
 
17
18
  def call
18
- query_type = build_type('Query', queries)
19
- mutation_type = build_type('Mutation', mutations)
19
+ query_type = build_group_type('Query', queries)
20
+ mutation_type = build_group_type('Mutation', mutations)
20
21
  raw = raw_actions
21
22
 
22
- GraphQL::Schema.define do
23
+ Class.new(GraphQL::Schema) do
23
24
  cursor_encoder(Router::PlainCursorEncoder)
24
25
  raw.each { |action| send(action[:name], *action[:args], &action[:block]) }
25
26
 
26
- query(query_type)
27
- mutation(mutation_type)
27
+ query(query_type) if query_type
28
+ mutation(mutation_type) if mutation_type
28
29
  end
29
30
  end
30
31
 
31
32
  private
32
33
 
33
- def build_type(type_name, routes)
34
- GraphQL::ObjectType.define do
35
- name type_name
34
+ attr_reader :group
36
35
 
37
- routes.each do |route|
38
- field route.name, function: route.function
36
+ def build_group_type(type_name, routes)
37
+ group_name = group
38
+ group_routes = routes.select { |route| route.show_in_group?(group_name) }
39
+ return if group_routes.empty?
40
+
41
+ build_type(type_name, group_routes)
42
+ end
43
+
44
+ def build_type(type_name, group_routes)
45
+ Class.new(GraphQL::Schema::Object) do
46
+ graphql_name(type_name)
47
+
48
+ group_routes.each do |route|
49
+ field(*route.field_args)
50
+ end
51
+
52
+ def self.inspect
53
+ "#{GraphQL::Schema::Object}(#{graphql_name})"
39
54
  end
40
55
  end
41
56
  end