graphql_rails 0.7.0 → 1.2.1

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 (82) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +1 -0
  3. data/.rubocop.yml +3 -3
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +2 -2
  6. data/CHANGELOG.md +35 -0
  7. data/Gemfile +3 -2
  8. data/Gemfile.lock +181 -71
  9. data/docs/README.md +40 -8
  10. data/docs/_sidebar.md +5 -0
  11. data/docs/components/controller.md +295 -9
  12. data/docs/components/decorator.md +69 -0
  13. data/docs/components/model.md +267 -6
  14. data/docs/components/routes.md +28 -0
  15. data/docs/getting_started/quick_start.md +10 -3
  16. data/docs/index.html +1 -1
  17. data/docs/logging_and_monitoring/logging_and_monitoring.md +35 -0
  18. data/docs/other_tools/query_runner.md +49 -0
  19. data/docs/other_tools/schema_dump.md +29 -0
  20. data/docs/testing/testing.md +3 -1
  21. data/graphql_rails.gemspec +5 -4
  22. data/lib/generators/graphql_rails/install_generator.rb +50 -0
  23. data/lib/generators/graphql_rails/templates/example_users_controller.erb +19 -0
  24. data/lib/generators/graphql_rails/templates/graphql_application_controller.erb +8 -0
  25. data/lib/generators/graphql_rails/templates/graphql_controller.erb +20 -0
  26. data/lib/generators/graphql_rails/templates/graphql_router.erb +19 -0
  27. data/lib/generators/graphql_rails/templates/graphql_router_spec.erb +21 -0
  28. data/lib/graphql_rails.rb +6 -0
  29. data/lib/graphql_rails/attributes/attributable.rb +22 -17
  30. data/lib/graphql_rails/attributes/attribute.rb +67 -3
  31. data/lib/graphql_rails/attributes/attribute_name_parser.rb +4 -4
  32. data/lib/graphql_rails/attributes/input_attribute.rb +33 -15
  33. data/lib/graphql_rails/attributes/input_type_parser.rb +62 -0
  34. data/lib/graphql_rails/attributes/type_name_info.rb +38 -0
  35. data/lib/graphql_rails/attributes/type_parseable.rb +132 -0
  36. data/lib/graphql_rails/attributes/type_parser.rb +59 -53
  37. data/lib/graphql_rails/concerns/service.rb +19 -0
  38. data/lib/graphql_rails/controller.rb +42 -21
  39. data/lib/graphql_rails/controller/action.rb +12 -67
  40. data/lib/graphql_rails/controller/action_configuration.rb +70 -28
  41. data/lib/graphql_rails/controller/build_controller_action_resolver.rb +52 -0
  42. data/lib/graphql_rails/controller/build_controller_action_resolver/controller_action_resolver.rb +28 -0
  43. data/lib/graphql_rails/controller/configuration.rb +56 -3
  44. data/lib/graphql_rails/controller/log_controller_action.rb +71 -0
  45. data/lib/graphql_rails/controller/request.rb +29 -8
  46. data/lib/graphql_rails/controller/request/format_errors.rb +58 -0
  47. data/lib/graphql_rails/decorator.rb +41 -0
  48. data/lib/graphql_rails/decorator/relation_decorator.rb +75 -0
  49. data/lib/graphql_rails/errors/custom_execution_error.rb +22 -0
  50. data/lib/graphql_rails/errors/execution_error.rb +6 -7
  51. data/lib/graphql_rails/errors/system_error.rb +14 -0
  52. data/lib/graphql_rails/errors/validation_error.rb +1 -5
  53. data/lib/graphql_rails/input_configurable.rb +47 -0
  54. data/lib/graphql_rails/integrations.rb +19 -0
  55. data/lib/graphql_rails/integrations/lograge.rb +39 -0
  56. data/lib/graphql_rails/integrations/sentry.rb +34 -0
  57. data/lib/graphql_rails/model.rb +26 -4
  58. data/lib/graphql_rails/model/add_fields_to_graphql_type.rb +45 -0
  59. data/lib/graphql_rails/model/build_connection_type.rb +52 -0
  60. data/lib/graphql_rails/model/{configuration → build_connection_type}/count_items.rb +5 -5
  61. data/lib/graphql_rails/model/build_enum_type.rb +39 -10
  62. data/lib/graphql_rails/model/build_graphql_input_type.rb +8 -4
  63. data/lib/graphql_rails/model/call_graphql_model_method.rb +72 -0
  64. data/lib/graphql_rails/model/configurable.rb +6 -2
  65. data/lib/graphql_rails/model/configuration.rb +30 -16
  66. data/lib/graphql_rails/model/find_or_build_graphql_type.rb +64 -0
  67. data/lib/graphql_rails/model/find_or_build_graphql_type_class.rb +46 -0
  68. data/lib/graphql_rails/model/input.rb +11 -7
  69. data/lib/graphql_rails/query_runner.rb +68 -0
  70. data/lib/graphql_rails/railtie.rb +10 -0
  71. data/lib/graphql_rails/router.rb +40 -13
  72. data/lib/graphql_rails/router/resource_routes_builder.rb +10 -9
  73. data/lib/graphql_rails/router/route.rb +21 -6
  74. data/lib/graphql_rails/router/schema_builder.rb +30 -11
  75. data/lib/graphql_rails/rspec_controller_helpers.rb +6 -4
  76. data/lib/graphql_rails/tasks/dump_graphql_schema.rb +57 -0
  77. data/lib/graphql_rails/tasks/schema.rake +14 -0
  78. data/lib/graphql_rails/version.rb +1 -1
  79. metadata +70 -19
  80. data/lib/graphql_rails/controller/controller_function.rb +0 -50
  81. data/lib/graphql_rails/controller/format_results.rb +0 -36
  82. data/lib/graphql_rails/model/build_graphql_type.rb +0 -37
@@ -0,0 +1,72 @@
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
+ formatted_arguments = formatted_method_input(custom_keyword_arguments)
35
+ model.send(method_name, **formatted_arguments)
36
+ end
37
+ end
38
+
39
+ def formatted_method_input(keyword_arguments)
40
+ keyword_arguments.transform_values do |input_argument|
41
+ formatted_method_input_argument(input_argument)
42
+ end
43
+ end
44
+
45
+ def formatted_method_input_argument(argument)
46
+ return argument.to_h if argument.is_a?(GraphQL::Schema::InputObject)
47
+
48
+ argument
49
+ end
50
+
51
+ def method_name
52
+ attribute_config.property
53
+ end
54
+
55
+ def paginated?
56
+ attribute_config.paginated?
57
+ end
58
+
59
+ def custom_keyword_arguments
60
+ return method_keyword_arguments unless paginated?
61
+
62
+ method_keyword_arguments.except(*PAGINATION_KEYS)
63
+ end
64
+
65
+ def with_graphql_context
66
+ return yield unless model.respond_to?(:with_graphql_context)
67
+
68
+ model.with_graphql_context(graphql_context) { yield }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -9,11 +9,15 @@ module GraphqlRails
9
9
  @attributes ||= {}
10
10
  end
11
11
 
12
- def name(type_name = nil)
13
- @name = type_name if type_name
12
+ def name(graphql_name = nil)
13
+ @name = graphql_name if graphql_name
14
14
  @name || default_name
15
15
  end
16
16
 
17
+ def type_name
18
+ @type_name ||= "#{name.camelize}Type#{SecureRandom.hex}"
19
+ end
20
+
17
21
  def description(new_description = nil)
18
22
  @description = new_description if new_description
19
23
  @description
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'graphql_rails/attributes'
4
- require 'graphql_rails/model/build_graphql_type'
4
+ require 'graphql_rails/model/find_or_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
@@ -17,13 +17,28 @@ module GraphqlRails
17
17
  @model_class = model_class
18
18
  end
19
19
 
20
- def attribute(attribute_name, type: nil, **attribute_options)
21
- attributes[attribute_name.to_s] = \
22
- Attributes::Attribute.new(
23
- attribute_name,
24
- type,
25
- attribute_options
26
- )
20
+ def initialize_copy(other)
21
+ super
22
+ @connection_type = nil
23
+ @graphql_type = nil
24
+ @input = other.instance_variable_get(:@input)&.transform_values(&:dup)
25
+ @attributes = other.instance_variable_get(:@attributes)&.transform_values(&:dup)
26
+ end
27
+
28
+ def attribute(attribute_name, **attribute_options)
29
+ key = attribute_name.to_s
30
+
31
+ attributes[key] ||= Attributes::Attribute.new(attribute_name)
32
+
33
+ attributes[key].tap do |attribute|
34
+ attribute_options.each do |method_name, args|
35
+ send_args = [method_name]
36
+ send_args << args if attribute.method(method_name).parameters.present?
37
+ attribute.public_send(*send_args)
38
+ end
39
+
40
+ yield(attribute) if block_given?
41
+ end
27
42
  end
28
43
 
29
44
  def input(input_name = nil)
@@ -41,17 +56,16 @@ module GraphqlRails
41
56
  end
42
57
 
43
58
  def graphql_type
44
- @graphql_type ||= BuildGraphqlType.call(
45
- name: name, description: description, attributes: attributes
59
+ @graphql_type ||= FindOrBuildGraphqlType.call(
60
+ name: name,
61
+ description: description,
62
+ attributes: attributes,
63
+ type_name: type_name
46
64
  )
47
65
  end
48
66
 
49
67
  def connection_type
50
- @connection_type ||= begin
51
- graphql_type.define_connection do
52
- field :total, types.Int, resolve: CountItems
53
- end
54
- end
68
+ @connection_type ||= BuildConnectionType.call(graphql_type)
55
69
  end
56
70
 
57
71
  private
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Model
5
+ # stores information about model specific config, like attributes and types
6
+ class FindOrBuildGraphqlType
7
+ require 'graphql_rails/concerns/service'
8
+ require 'graphql_rails/model/find_or_build_graphql_type_class'
9
+ require 'graphql_rails/model/add_fields_to_graphql_type'
10
+
11
+ include ::GraphqlRails::Service
12
+
13
+ def initialize(name:, description:, attributes:, type_name:)
14
+ @name = name
15
+ @description = description
16
+ @attributes = attributes
17
+ @type_name = type_name
18
+ end
19
+
20
+ def call
21
+ klass.tap { add_fields_to_graphql_type if new_class? }
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :name, :description, :attributes, :type_name
27
+
28
+ delegate :klass, :new_class?, to: :type_class_finder
29
+
30
+ def type_class_finder
31
+ @type_class_finder ||= FindOrBuildGraphqlTypeClass.new(
32
+ name: name,
33
+ type_name: type_name,
34
+ description: description
35
+ )
36
+ end
37
+
38
+ def add_fields_to_graphql_type
39
+ AddFieldsToGraphqlType.call(klass: klass, attributes: attributes.values.select(&:scalar_type?))
40
+
41
+ attributes.values.reject(&:scalar_type?).tap do |dynamic_attributes|
42
+ find_or_build_dynamic_graphql_types(dynamic_attributes) do |name, description, attributes, type_name|
43
+ self.class.call(
44
+ name: name, description: description,
45
+ attributes: attributes, type_name: type_name
46
+ )
47
+ end
48
+ AddFieldsToGraphqlType.call(klass: klass, attributes: dynamic_attributes)
49
+ end
50
+ end
51
+
52
+ def find_or_build_dynamic_graphql_types(dynamic_attributes)
53
+ dynamic_attributes.each do |attribute|
54
+ yield(
55
+ attribute.graphql_model.graphql.name,
56
+ attribute.graphql_model.graphql.description,
57
+ attribute.graphql_model.graphql.attributes,
58
+ attribute.graphql_model.graphql.type_name
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphqlRails
4
+ module Model
5
+ # Initializes class to define graphql type and fields.
6
+ class FindOrBuildGraphqlTypeClass
7
+ require 'graphql_rails/concerns/service'
8
+
9
+ include ::GraphqlRails::Service
10
+
11
+ def initialize(name:, type_name:, description: nil)
12
+ @name = name
13
+ @type_name = type_name
14
+ @description = description
15
+ @new_class = false
16
+ end
17
+
18
+ def klass
19
+ @klass ||= Object.const_defined?(type_name) && Object.const_get(type_name) || build_graphql_type_klass
20
+ end
21
+
22
+ def new_class?
23
+ new_class
24
+ end
25
+
26
+ private
27
+
28
+ attr_accessor :new_class
29
+ attr_reader :name, :type_name, :description
30
+
31
+ def build_graphql_type_klass
32
+ graphql_type_name = name
33
+ graphql_type_description = description
34
+
35
+ graphql_type_klass = Class.new(GraphQL::Schema::Object) do
36
+ graphql_name(graphql_type_name)
37
+ description(graphql_type_description)
38
+ end
39
+
40
+ self.new_class = true
41
+
42
+ Object.const_set(type_name, graphql_type_klass)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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
@@ -35,7 +39,7 @@ module GraphqlRails
35
39
 
36
40
  def default_name
37
41
  @default_name ||= begin
38
- suffix = input_name_suffix ? input_name_suffix.to_s.tableize : ''
42
+ suffix = input_name_suffix ? input_name_suffix.to_s.camelize : ''
39
43
  "#{model_class.name.split('::').last}#{suffix}Input"
40
44
  end
41
45
  end
@@ -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