rails-graphql 1.0.0.beta → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/ext/gql_parser.c +1 -16
  3. data/ext/gql_parser.h +21 -0
  4. data/ext/shared.c +0 -5
  5. data/ext/shared.h +6 -6
  6. data/lib/generators/graphql/channel_generator.rb +27 -0
  7. data/lib/generators/graphql/controller_generator.rb +9 -4
  8. data/lib/generators/graphql/install_generator.rb +49 -0
  9. data/lib/generators/graphql/schema_generator.rb +9 -4
  10. data/lib/generators/graphql/templates/channel.erb +7 -0
  11. data/lib/generators/graphql/templates/config.rb +97 -0
  12. data/lib/generators/graphql/templates/controller.erb +2 -0
  13. data/lib/generators/graphql/templates/schema.erb +5 -3
  14. data/lib/gql_parser.so +0 -0
  15. data/lib/rails/graphql/alternative/field_set.rb +12 -0
  16. data/lib/rails/graphql/alternative/query.rb +9 -4
  17. data/lib/rails/graphql/alternative/subscription.rb +2 -1
  18. data/lib/rails/graphql/argument.rb +5 -3
  19. data/lib/rails/graphql/callback.rb +8 -7
  20. data/lib/rails/graphql/collectors/hash_collector.rb +12 -1
  21. data/lib/rails/graphql/collectors/json_collector.rb +21 -0
  22. data/lib/rails/graphql/config.rb +73 -57
  23. data/lib/rails/graphql/directive/include_directive.rb +0 -1
  24. data/lib/rails/graphql/directive/skip_directive.rb +0 -1
  25. data/lib/rails/graphql/directive/specified_by_directive.rb +24 -0
  26. data/lib/rails/graphql/directive.rb +30 -24
  27. data/lib/rails/graphql/event.rb +7 -6
  28. data/lib/rails/graphql/field/authorized_field.rb +0 -5
  29. data/lib/rails/graphql/field/input_field.rb +0 -5
  30. data/lib/rails/graphql/field/mutation_field.rb +5 -6
  31. data/lib/rails/graphql/field/output_field.rb +13 -2
  32. data/lib/rails/graphql/field/proxied_field.rb +5 -5
  33. data/lib/rails/graphql/field/resolved_field.rb +1 -1
  34. data/lib/rails/graphql/field/subscription_field.rb +35 -52
  35. data/lib/rails/graphql/field/typed_field.rb +26 -2
  36. data/lib/rails/graphql/field.rb +20 -19
  37. data/lib/rails/graphql/global_id.rb +5 -1
  38. data/lib/rails/graphql/helpers/inherited_collection/array.rb +1 -0
  39. data/lib/rails/graphql/helpers/inherited_collection/base.rb +2 -0
  40. data/lib/rails/graphql/helpers/inherited_collection/hash.rb +2 -1
  41. data/lib/rails/graphql/helpers/registerable.rb +1 -1
  42. data/lib/rails/graphql/helpers/with_arguments.rb +3 -2
  43. data/lib/rails/graphql/helpers/with_callbacks.rb +3 -3
  44. data/lib/rails/graphql/helpers/with_description.rb +10 -8
  45. data/lib/rails/graphql/helpers/with_directives.rb +5 -1
  46. data/lib/rails/graphql/helpers/with_events.rb +1 -0
  47. data/lib/rails/graphql/helpers/with_fields.rb +28 -22
  48. data/lib/rails/graphql/helpers/with_name.rb +3 -2
  49. data/lib/rails/graphql/helpers/with_schema_fields.rb +72 -48
  50. data/lib/rails/graphql/introspection.rb +1 -1
  51. data/lib/rails/graphql/railtie.rb +3 -2
  52. data/lib/rails/graphql/railties/app/base_channel.rb +10 -0
  53. data/lib/rails/graphql/railties/app/base_controller.rb +12 -0
  54. data/lib/rails/graphql/railties/app/views/_cable.js.erb +56 -0
  55. data/lib/rails/graphql/railties/app/views/_fetch.js.erb +20 -0
  56. data/lib/rails/graphql/railties/app/views/graphiql.html.erb +101 -0
  57. data/lib/rails/graphql/railties/base_generator.rb +3 -9
  58. data/lib/rails/graphql/railties/channel.rb +8 -8
  59. data/lib/rails/graphql/railties/controller.rb +45 -24
  60. data/lib/rails/graphql/request/arguments.rb +2 -1
  61. data/lib/rails/graphql/request/backtrace.rb +31 -10
  62. data/lib/rails/graphql/request/component/field.rb +15 -8
  63. data/lib/rails/graphql/request/component/fragment.rb +13 -7
  64. data/lib/rails/graphql/request/component/operation/subscription.rb +4 -6
  65. data/lib/rails/graphql/request/component/operation.rb +11 -4
  66. data/lib/rails/graphql/request/component/spread.rb +13 -4
  67. data/lib/rails/graphql/request/component/typename.rb +1 -1
  68. data/lib/rails/graphql/request/component.rb +2 -0
  69. data/lib/rails/graphql/request/context.rb +1 -1
  70. data/lib/rails/graphql/request/event.rb +6 -2
  71. data/lib/rails/graphql/request/helpers/directives.rb +1 -0
  72. data/lib/rails/graphql/request/helpers/selection_set.rb +10 -4
  73. data/lib/rails/graphql/request/helpers/value_writers.rb +8 -5
  74. data/lib/rails/graphql/request/prepared_data.rb +3 -1
  75. data/lib/rails/graphql/request/steps/organizable.rb +1 -1
  76. data/lib/rails/graphql/request/steps/preparable.rb +1 -1
  77. data/lib/rails/graphql/request/steps/resolvable.rb +1 -1
  78. data/lib/rails/graphql/request/strategy/sequenced_strategy.rb +3 -3
  79. data/lib/rails/graphql/request/strategy.rb +18 -4
  80. data/lib/rails/graphql/request/subscription.rb +18 -16
  81. data/lib/rails/graphql/request.rb +67 -37
  82. data/lib/rails/graphql/schema.rb +39 -86
  83. data/lib/rails/graphql/shortcuts.rb +11 -5
  84. data/lib/rails/graphql/source/active_record/builders.rb +20 -21
  85. data/lib/rails/graphql/source/active_record_source.rb +93 -33
  86. data/lib/rails/graphql/source/base.rb +11 -39
  87. data/lib/rails/graphql/source/builder.rb +9 -22
  88. data/lib/rails/graphql/source/scoped_arguments.rb +10 -4
  89. data/lib/rails/graphql/source.rb +23 -37
  90. data/lib/rails/graphql/subscription/provider/action_cable.rb +10 -9
  91. data/lib/rails/graphql/subscription/provider/base.rb +6 -5
  92. data/lib/rails/graphql/subscription/store/base.rb +5 -9
  93. data/lib/rails/graphql/subscription/store/memory.rb +18 -9
  94. data/lib/rails/graphql/type/creator.rb +196 -0
  95. data/lib/rails/graphql/type/enum.rb +17 -9
  96. data/lib/rails/graphql/type/input.rb +20 -4
  97. data/lib/rails/graphql/type/interface.rb +15 -4
  98. data/lib/rails/graphql/type/object/directive_object.rb +6 -5
  99. data/lib/rails/graphql/type/object/input_value_object.rb +3 -4
  100. data/lib/rails/graphql/type/object/type_object.rb +40 -13
  101. data/lib/rails/graphql/type/object.rb +10 -5
  102. data/lib/rails/graphql/type/scalar/binary_scalar.rb +2 -0
  103. data/lib/rails/graphql/type/scalar/date_scalar.rb +2 -0
  104. data/lib/rails/graphql/type/scalar/date_time_scalar.rb +2 -0
  105. data/lib/rails/graphql/type/scalar/decimal_scalar.rb +2 -0
  106. data/lib/rails/graphql/type/scalar/json_scalar.rb +3 -1
  107. data/lib/rails/graphql/type/scalar/time_scalar.rb +3 -1
  108. data/lib/rails/graphql/type/scalar.rb +1 -1
  109. data/lib/rails/graphql/type/union.rb +7 -2
  110. data/lib/rails/graphql/type.rb +10 -2
  111. data/lib/rails/graphql/type_map.rb +18 -7
  112. data/lib/rails/graphql/uri.rb +5 -4
  113. data/lib/rails/graphql/version.rb +6 -2
  114. data/lib/rails/graphql.rb +9 -7
  115. data/test/assets/introspection-mem.txt +1 -1
  116. data/test/assets/introspection.gql +2 -0
  117. data/test/assets/mem.gql +74 -60
  118. data/test/assets/mysql.gql +69 -55
  119. data/test/assets/sqlite.gql +78 -64
  120. data/test/assets/translate.gql +50 -39
  121. data/test/config.rb +2 -1
  122. data/test/graphql/schema_test.rb +2 -31
  123. data/test/graphql/source_test.rb +0 -10
  124. data/test/graphql/type/interface_test.rb +8 -5
  125. data/test/graphql/type/object_test.rb +8 -2
  126. data/test/graphql/type_map_test.rb +13 -16
  127. data/test/integration/global_id_test.rb +4 -4
  128. data/test/integration/memory/star_wars_validation_test.rb +2 -2
  129. data/test/integration/mysql/star_wars_introspection_test.rb +1 -1
  130. data/test/integration/resolver_precedence_test.rb +1 -1
  131. data/test/integration/schemas/memory.rb +3 -4
  132. data/test/integration/sqlite/star_wars_global_id_test.rb +27 -21
  133. data/test/integration/sqlite/star_wars_introspection_test.rb +1 -1
  134. data/test/integration/translate_test.rb +26 -14
  135. metadata +20 -7
@@ -39,7 +39,7 @@ module Rails
39
39
  stack = [component] + request.stack
40
40
  counter = stack.count { |item| !item.is_a?(Numeric) }
41
41
  objects = request.strategy.context
42
- oidx = -1
42
+ oid = -1
43
43
 
44
44
  last_object = suffix = nil
45
45
  while (item = stack.shift)
@@ -51,11 +51,13 @@ module Rails
51
51
  add = send("row_for_#{item.kind}", data, item, suffix)
52
52
 
53
53
  if item.kind == :field
54
- oidx += 1
55
- data[5] ||= oidx == 0 ? '↓' : print_object(objects&.at(oidx - 1))
56
- data[3] ||= print_object(objects&.at(oidx))
54
+ oid += 1
55
+ data[5] ||= oid == 0 ? '↓' : print_object(objects&.at(oid - 1))
56
+ data[3] ||= print_object(objects&.at(oid))
57
57
  end
58
58
 
59
+ data[4] = clean_arguments(data[4], request) if data[4]
60
+
59
61
  suffix = nil
60
62
  counter -= 1
61
63
  add_to_table(table, data) if add != false
@@ -67,7 +69,7 @@ module Rails
67
69
  # Print the backtrace steps of the error
68
70
  def print_backtrace(error, request)
69
71
  steps = error.backtrace
70
- steps = cleaner.clean(steps) unless cleaner.nil?
72
+ # steps = cleaner.clean(steps) unless cleaner.nil?
71
73
 
72
74
  klass = +"(\e[4m#{error.class}\e[24m)"
73
75
  stage = +" [#{request.strategy.stage}]" if skip_base_class != StandardError
@@ -122,26 +124,45 @@ module Rails
122
124
  object.respond_to?(:to_gql_backtrace) ? object.to_gql_backtrace : object.inspect
123
125
  end
124
126
 
127
+ # Make sure to properly parse arguments and filter them
128
+ def clean_arguments(arguments, request)
129
+ value = arguments.as_json
130
+ return '{}' if value.blank?
131
+
132
+ request.cache(:backtrace_arguments_filter) do
133
+ ActiveSupport::ParameterFilter.new(GraphQL.config.filter_parameters)
134
+ end.filter(value)
135
+ end
136
+
125
137
  # Visitors
126
138
  def row_for_field(data, item, suffix)
127
139
  field = item.field
128
- name = +"#{field.owner.gql_name}.#{field.gql_name}#{suffix}" unless field.nil?
140
+ parent =
141
+ if !field
142
+ '*'
143
+ elsif field.owner.is_a?(Helpers::WithSchemaFields)
144
+ item.request.schema.type_name_for(field.schema_type)
145
+ else
146
+ field.owner.gql_name
147
+ end
148
+
149
+ name = +"#{parent}.#{field.gql_name}#{suffix}" unless field.nil?
129
150
 
130
151
  data.push(name || +"*.#{item.name}")
131
- data.push(nil, (item.arguments ? item.arguments.to_json : 'nil'), nil)
152
+ data.push(nil, item.arguments, nil)
132
153
  end
133
154
 
134
155
  def row_for_fragment(data, item, *)
135
156
  type = item.instance_variable_get(:@node)[1]
136
157
  object = item.current_object || item.type_klass
137
- data.push(+"fragment #{item.name}", type, '')
158
+ data.push(+"fragment #{item.name}", type, nil)
138
159
  data.push(print_object(object))
139
160
  end
140
161
 
141
162
  def row_for_operation(data, item, *)
142
163
  data.push(+"#{item.type} #{item.name}".squish)
143
164
  data.push('nil')
144
- data.push(item.arguments.to_json)
165
+ data.push(item.variables)
145
166
  data.push(item.typename)
146
167
  end
147
168
 
@@ -150,7 +171,7 @@ module Rails
150
171
 
151
172
  type = item.instance_variable_get(:@node)[1]
152
173
  object = item.current_object || item.type_klass
153
- data.push('...', type, '')
174
+ data.push('...', type, nil)
154
175
  data.push(print_object(object))
155
176
  end
156
177
 
@@ -15,7 +15,7 @@ module Rails
15
15
 
16
16
  delegate :decorate, to: :type_klass
17
17
  delegate :operation, :variables, :request, to: :parent
18
- delegate :method_name, :resolver, :performer, :type_klass,:leaf_type?,
18
+ delegate :method_name, :resolver, :performer, :type_klass, :leaf_type?,
19
19
  :dynamic_resolver?, :mutation?, to: :field
20
20
 
21
21
  attr_reader :name, :alias_name, :parent, :field, :arguments, :current_object
@@ -97,10 +97,11 @@ module Rails
97
97
  (try(:current_object) || try(:type_klass))&.gql_name
98
98
  end
99
99
 
100
- # Check if the field is an entry point, meaning that its parent is the
101
- # operation and it is associated to a schema field
100
+ # Check if the field is an entry point, meaning that it is attached to
101
+ # an owner that has Schema Fields
102
102
  def entry_point?
103
- parent.kind === :operation
103
+ return @entry_point if defined?(@entry_point)
104
+ @entry_point = field.entry_point?
104
105
  end
105
106
 
106
107
  # Fields are assignable because they are actually the selection, so they
@@ -117,6 +118,12 @@ module Rails
117
118
  value != false
118
119
  end
119
120
 
121
+ # Override this to also check if the key would be added to the response
122
+ # again
123
+ def skipped?
124
+ super || response.key?(gql_name)
125
+ end
126
+
120
127
  # A little extension of the +is_a?+ method that allows checking it using
121
128
  # the underlying +field+
122
129
  def of_type?(klass)
@@ -150,6 +157,7 @@ module Rails
150
157
  end
151
158
 
152
159
  # Build the cache object
160
+ # TODO: Add the arguments into the GID, but the problem is variables
153
161
  def cache_dump
154
162
  super.merge(field: (field && all_to_gid(field)))
155
163
  end
@@ -172,10 +180,10 @@ module Rails
172
180
  check_assignment!
173
181
 
174
182
  parse_directives(@node[3])
175
- check_authorization!
176
-
177
183
  parse_arguments(@node[2])
178
184
  parse_selection(@node[4])
185
+
186
+ check_authorization!
179
187
  end
180
188
  end
181
189
 
@@ -221,8 +229,7 @@ module Rails
221
229
  # Check if the field was assigned correctly to an output field
222
230
  def check_assignment!
223
231
  raise MissingFieldError, (+<<~MSG).squish if field.nil?
224
- Unable to find a field named "#{gql_name}" on
225
- #{entry_point? ? operation.kind : parent.type_klass.name}.
232
+ Unable to find a field named "#{gql_name}" on #{parent.typename}.
226
233
  MSG
227
234
 
228
235
  raise FieldError, (+<<~MSG).squish unless field.output_type?
@@ -23,6 +23,16 @@ module Rails
23
23
  check_duplicated_fragment!
24
24
  end
25
25
 
26
+ # Check if all the sub fields are broadcastable
27
+ def broadcastable?
28
+ selection.each_value.all?(&:broadcastable?)
29
+ end
30
+
31
+ # Check if the fragment has been prepared already
32
+ def prepared?
33
+ defined?(@prepared) && @prepared
34
+ end
35
+
26
36
  # Return a lazy loaded variable proc
27
37
  def variables
28
38
  Request::Arguments.lazy
@@ -59,11 +69,6 @@ module Rails
59
69
  @current_object = nil
60
70
  end
61
71
 
62
- # Check if all the sub fields are broadcastable
63
- def broadcastable?
64
- selection.each_value.all?(&:broadcastable?)
65
- end
66
-
67
72
  # Build the cache object
68
73
  def cache_dump
69
74
  super.merge(type_klass: all_to_gid(type_klass))
@@ -105,8 +110,9 @@ module Rails
105
110
  def resolve
106
111
  return if unresolvable?
107
112
 
108
- object = @current_object || type_klass
109
- resolve_then if type_klass =~ object
113
+ type = type_klass
114
+ object = @current_object
115
+ resolve_then if (object.nil? && type&.operational?) || type =~ object
110
116
  end
111
117
 
112
118
  # This will just trigger the selection resolver
@@ -89,14 +89,12 @@ module Rails
89
89
 
90
90
  # Rewrite this method so that the subscription can be generated in
91
91
  # the right place
92
- def resolve_then(after_block = nil, &block)
93
- subscribe_block = -> do
92
+ def resolve_then(&block)
93
+ super do
94
94
  save_subscription
95
- trigger_event(:subscribed)
96
- after_block.call if after_block.present?
95
+ trigger_event(:subscribed, subscription: subscription)
96
+ block.call if block.present?
97
97
  end
98
-
99
- super(subscribe_block, &block)
100
98
  end
101
99
 
102
100
  # Save the subscription using the schema subscription provider
@@ -73,6 +73,13 @@ module Rails
73
73
  schema.fields_for(type)
74
74
  end
75
75
 
76
+ # Allow accessing the fake type form the schema. It's used for
77
+ # inline spreads without a specified type
78
+ def type_klass
79
+ return @type_klass if defined?(@type_klass)
80
+ @type_klass = schema.public_send("#{type}_type")
81
+ end
82
+
76
83
  # The typename is always based on the fake name used for the set of
77
84
  # schema fields
78
85
  def typename
@@ -109,7 +116,7 @@ module Rails
109
116
  return super unless defined?(@used_fragments)
110
117
 
111
118
  super ^ used_fragments.reduce(0) do |value, fragment|
112
- request.fragments[fragment].hash
119
+ value ^ request.fragments[fragment].hash
113
120
  end
114
121
  end
115
122
 
@@ -129,8 +136,8 @@ module Rails
129
136
 
130
137
  # Trigger an specific event with the +type+ of the operation
131
138
  def organize
139
+ trigger_event(type)
132
140
  organize_then do
133
- trigger_event(type)
134
141
  yield if block_given?
135
142
  organize_fields
136
143
  report_unused_variables
@@ -147,8 +154,8 @@ module Rails
147
154
  end
148
155
 
149
156
  # Resolve all the fields
150
- def resolve
151
- resolve_then { resolve_fields }
157
+ def resolve_then(&block)
158
+ super(block) { resolve_fields }
152
159
  end
153
160
 
154
161
  # Don't stack over response when the operation doesn't have a name
@@ -95,7 +95,7 @@ module Rails
95
95
  def organize_then(&block)
96
96
  super(block) do
97
97
  if inline?
98
- @type_klass = find_type!(@node[1])
98
+ @type_klass = @node[1].nil? ? parent.type_klass : find_type!(@node[1])
99
99
  parse_directives(@node[2])
100
100
  parse_selection(@node[3])
101
101
  else
@@ -111,17 +111,26 @@ module Rails
111
111
  # Spread has a special behavior when using a fragment
112
112
  def prepare
113
113
  return super if inline?
114
- raise(+'Prepare with fragment not implemented yet')
114
+ catch(:fragment_prepared) do
115
+ return fragment.prepare!
116
+ end
117
+
118
+ invalidate!
119
+ raise ExecutionError, (+<<~MSG).squish
120
+ Fields inside the "#{name}" fragment tried to be prepared more than once.
121
+ This feature is not supported yet.
122
+ MSG
115
123
  end
116
124
 
117
125
  # Resolve the spread operation
118
126
  def resolve
119
127
  return if unresolvable?
120
128
 
121
- object = (defined?(@current_object) && @current_object) || parent.type_klass
129
+ object = (defined?(@current_object) && @current_object)
130
+ object ||= parent.type_klass unless parent.kind == :operation
122
131
  return run_on_fragment(:resolve_with!, object) unless inline?
123
132
 
124
- super if type_klass =~ object
133
+ super if (object.nil? && type_klass&.operational?) || type_klass =~ object
125
134
  end
126
135
 
127
136
  # This will just trigger the selection resolver
@@ -45,7 +45,7 @@ module Rails
45
45
 
46
46
  # Write the typename information
47
47
  def write_value(value)
48
- response.serialize(Type::Scalar::StringScalar, gql_name, value)
48
+ response.serialize(Type::Scalar::StringScalar, gql_name, value.itself)
49
49
  end
50
50
 
51
51
  # Typename is always broadcastable
@@ -111,6 +111,8 @@ module Rails
111
111
  # Run a given block and ensure to capture exceptions to set them as
112
112
  # errors
113
113
  def report_exception(error)
114
+ return if request.rescue_with_handler(error, source: self) == false
115
+
114
116
  Backtrace.print(error, self, request)
115
117
 
116
118
  stack_path = request.stack_to_path
@@ -43,7 +43,7 @@ module Rails
43
43
  at(1)
44
44
  end
45
45
 
46
- # Get the current value, which basically means basically the first item
46
+ # Get the current value, which basically means the first item
47
47
  # on the current stack
48
48
  def current_value
49
49
  at(0)
@@ -10,10 +10,10 @@ module Rails
10
10
  class Event < GraphQL::Event
11
11
  OBJECT_BASED_READERS = %i[fragment spread].freeze
12
12
 
13
- delegate :errors, :context, to: :request
13
+ delegate :errors, :context, :extensions, to: :request
14
14
  delegate :instance_for, to: :strategy
15
15
  delegate :memo, :schema, to: :source
16
- delegate :subscription_field, to: :schema
16
+ delegate :subscription_provider, to: :schema
17
17
 
18
18
  attr_reader :strategy, :request, :index
19
19
 
@@ -34,12 +34,16 @@ module Rails
34
34
  super(name, source, **data)
35
35
  end
36
36
 
37
+ # TODO: Implement a faster way to check if if the event is from the
38
+ # same source by separating exclusive events beforehand
39
+
37
40
  # If the source is a field, than also compare to the actual field
38
41
  def same_source?(other)
39
42
  super || (source.try(:kind) == :field && source.field == other)
40
43
  end
41
44
 
42
45
  # Provide a way to access the current field value
46
+ # TODO: Maybe change this to +current+ to get the value by reference
43
47
  def current_value
44
48
  resolver&.current_value
45
49
  end
@@ -29,6 +29,7 @@ module Rails
29
29
  alias all_events directive_events
30
30
 
31
31
  # Check if the current component is using a directive
32
+ # TODO: This does not work with the instance
32
33
  def using?(item)
33
34
  return false unless directives?
34
35
 
@@ -55,7 +55,7 @@ module Rails
55
55
  # Using +fields_source+, find the needed ones to be assigned to the
56
56
  # current requested fields. As shown by benchmark, since the index is
57
57
  # based on Symbols, the best way to find +gql_name+ based fields is
58
- # through iteration and search. Complexity O(n)
58
+ # through iteration, then search and assign. Complexity O(n)
59
59
  def assign_fields!(assigners)
60
60
  pending = assigners.map(&:size).reduce(:+) || 0
61
61
  return if pending.zero?
@@ -72,19 +72,21 @@ module Rails
72
72
  # Recursive operation that perform the organization step for the
73
73
  # selection
74
74
  def organize_fields
75
- selection.each_value(&:organize!) if selection.present?
75
+ return unless run_selection?
76
+ selection.each_value(&:organize!)
76
77
  end
77
78
 
78
79
  # Find all the fields that have a prepare step and execute them
79
80
  def prepare_fields
80
- selection.each_value(&:prepare!) if selection.present?
81
+ return unless run_selection?
82
+ selection.each_value(&:prepare!)
81
83
  end
82
84
 
83
85
  # Trigger the process of resolving the value of all the fields. Since
84
86
  # complex object may or may not be inside an array, this helps to
85
87
  # decide if a new stack should be started or not
86
88
  def resolve_fields(object = nil)
87
- return unless selection.present?
89
+ return unless run_selection?
88
90
 
89
91
  items = selection.each_value
90
92
  items = items.each_with_object(object) unless object.nil?
@@ -96,6 +98,10 @@ module Rails
96
98
 
97
99
  private
98
100
 
101
+ def run_selection?
102
+ selection.present? && !unresolvable?
103
+ end
104
+
99
105
  def add_component(node)
100
106
  item_name = node[1] || node[0]
101
107
 
@@ -20,6 +20,8 @@ module Rails
20
20
 
21
21
  # Resolve a given value when it is an array
22
22
  def write_array(value, idx = -1, &block)
23
+ return write_leaf(value) if value.nil?
24
+
23
25
  write_array!(value) do |item|
24
26
  stacked(idx += 1) do
25
27
  block.call(item, idx)
@@ -30,16 +32,17 @@ module Rails
30
32
  block.call(nil, idx)
31
33
  response.next
32
34
 
33
- format_array_execption(error, idx)
35
+ format_array_exception(error, idx)
34
36
  request.exception_to_error(error, self)
35
37
  end
36
38
  rescue StandardError => error
37
- format_array_execption(error, idx)
39
+ format_array_exception(error, idx)
38
40
  raise
39
41
  end
40
42
  end
41
43
 
42
44
  # Helper to start writing as array
45
+ # TODO: Add the support for `iterator`
43
46
  def write_array!(value, &block)
44
47
  raise InvalidValueError, (+<<~MSG).squish unless value.respond_to?(:each)
45
48
  The #{gql_name} field is excepting an array
@@ -55,7 +58,7 @@ module Rails
55
58
  end
56
59
 
57
60
  # Add the item index to the exception message
58
- def format_array_execption(error, idx)
61
+ def format_array_exception(error, idx)
59
62
  real_error = (+<<~MSG).squish
60
63
  The #{ActiveSupport::Inflector.ordinalize(idx + 1)} value of the #{gql_name} field
61
64
  MSG
@@ -70,13 +73,13 @@ module Rails
70
73
 
71
74
  # Write a value based on a Union type
72
75
  def write_union(value)
73
- object = type_klass.all_members&.reverse_each&.find { |t| t.valid_member?(value) }
76
+ object = type_klass.type_for(value, request)
74
77
  object.nil? ? raise_invalid_member! : resolve_fields(object)
75
78
  end
76
79
 
77
80
  # Write a value based on a Interface type
78
81
  def write_interface(value)
79
- object = type_klass.all_types&.reverse_each&.find { |t| t.valid_member?(value) }
82
+ object = type_klass.type_for(value, request)
80
83
  object.nil? ? raise_invalid_member! : resolve_fields(object)
81
84
  end
82
85
 
@@ -17,6 +17,7 @@ module Rails
17
17
  REPEAT_OPTIONS = {
18
18
  true => true,
19
19
  false => 1,
20
+ once: 1,
20
21
  cycle: true,
21
22
  always: true,
22
23
  }.freeze
@@ -55,7 +56,8 @@ module Rails
55
56
  @field = field
56
57
  @value = value
57
58
  @array = value.is_a?(Array) && !field.array?
58
- @repeat =
59
+ @repeat = true if !@array && !value.is_a?(Array)
60
+ @repeat ||=
59
61
  case repeat
60
62
  when Numeric then repeat
61
63
  when Enumerator then repeat.size
@@ -70,8 +70,8 @@ module Rails
70
70
  stacked do
71
71
  block.call
72
72
  strategy.add_listeners_from(self)
73
- trigger_event(:organized)
74
73
  after_block.call if after_block.present?
74
+ trigger_event(:organized)
75
75
  end
76
76
  end
77
77
 
@@ -36,8 +36,8 @@ module Rails
36
36
 
37
37
  stacked do
38
38
  block.call if block.present?
39
- trigger_event(:prepared)
40
39
  after_block.call if after_block.present?
40
+ trigger_event(:prepared)
41
41
  end
42
42
  end
43
43
  end
@@ -31,8 +31,8 @@ module Rails
31
31
 
32
32
  stacked do
33
33
  block.call if block.present?
34
- trigger_event(:finalize)
35
34
  after_block.call if after_block.present?
35
+ trigger_event(:finalize)
36
36
  end
37
37
  end
38
38
  end
@@ -16,9 +16,9 @@ module Rails
16
16
  def resolve!
17
17
  response.with_stack('data') do
18
18
  for_each_operation do |op|
19
- collect_listeners { op.organize! }
20
- collect_data(true) { op.prepare! }
21
- collect_response { op.resolve! }
19
+ collect_listeners { op.organize! }
20
+ collect_data(op.mutation?) { op.prepare! }
21
+ collect_response { op.resolve! }
22
22
  end
23
23
  end
24
24
  end
@@ -92,6 +92,7 @@ module Rails
92
92
  # Execute the prepare step for the given +field+ and execute the given
93
93
  # block using context stack
94
94
  def prepare(field, &block)
95
+ check_fragment_multiple_prepare!(field)
95
96
  value = safe_store_data(field) do
96
97
  prepared = request.prepared_data_for(field)
97
98
  if prepared.is_a?(PreparedData)
@@ -131,7 +132,7 @@ module Rails
131
132
 
132
133
  if field.try(:dynamic_resolver?)
133
134
  prepared = prepared_data_for(field)
134
- args << Event.trigger(:resolve, field, self, prepared: prepared, &field.resolver)
135
+ args << Event.trigger(:resolve, field, self, prepared_data: prepared, &field.resolver)
135
136
  elsif field.prepared_data?
136
137
  args << prepared_data_for(field)
137
138
  else
@@ -279,6 +280,9 @@ module Rails
279
280
  @context = request.build(Request::Context)
280
281
 
281
282
  # TODO: Create an orchestrator to allow cross query loading
283
+ # TODO: We don't need to traverse over the fields, we can
284
+ # get the ones with such event and use parent to figure out
285
+ # the stack
282
286
  yield if force || listening_to?(:prepare)
283
287
  end
284
288
 
@@ -290,14 +294,24 @@ module Rails
290
294
 
291
295
  # Fetch the data for a given field and set as the first element
292
296
  # of the returned list
297
+ # TODO: Maybe implement root value to be returned by entry points
293
298
  def data_for(result, field)
294
299
  return result << @data_pool[field] if @data_pool.key?(field)
295
300
  return if field.entry_point?
296
301
 
297
- current, key = context.current_value, field.method_name
298
- return result << current.public_send(key) if current.respond_to?(key)
302
+ key = field.method_name
303
+ if (current = context.current_value).is_a?(::Hash)
304
+ result << (current.key?(key) ? current[key] : current[field.gql_name])
305
+ elsif current.respond_to?(key)
306
+ result << current.public_send(key)
307
+ end
308
+ end
299
309
 
300
- result << current[key] if current.respond_to?(:key?) && current.key?(key)
310
+ # If the data pool already have data for the given +field+ and there
311
+ # is a fragment in the stack, we throw back to the fragment
312
+ def check_fragment_multiple_prepare!(field)
313
+ return unless @data_pool.key?(field)
314
+ throw(:fragment_prepared) if request.stack.any?(Component::Fragment)
301
315
  end
302
316
  end
303
317
  end
@@ -6,34 +6,36 @@ module Rails
6
6
  # = GraphQL Request Subscription
7
7
  #
8
8
  # A simple object to store information about a generated subscription
9
- # TODO: Add a callback for the schema allowing it to prepare the context
10
- # before saving it into a subscription
9
+ # TODO: Maybe add a callback for the schema allowing it to prepare the
10
+ # context before saving it into a subscription
11
11
  class Subscription
12
12
  NULL_SCOPE = Object.new.freeze
13
13
 
14
14
  attr_reader :sid, :schema, :args, :field, :scope, :context, :broadcastable,
15
- :origin, :last_updated_at, :operation_id
15
+ :origin, :created_at, :updated_at, :operation_id
16
16
 
17
17
  alias broadcastable? broadcastable
18
+ alias id sid
18
19
 
19
20
  def initialize(request, operation)
20
- entrypoint = operation.selection.each_value.first
21
+ entrypoint = operation.selection.each_value.first # Memory Fingerprint
21
22
 
22
- @schema = request.schema.namespace
23
- @origin = request.origin
24
- @operation_id = operation.hash
25
- @args = entrypoint.arguments.to_h
26
- @field = entrypoint.field
27
- @context = request.context.to_h
28
- @broadcastable = operation.broadcastable?
29
- updated!
23
+ @schema = request.schema.namespace # 1 Symbol
24
+ @origin = request.origin # * HEAVY!
25
+ @operation_id = operation.hash # 1 Integer
26
+ @args = entrypoint.arguments.to_h # 1 Hash of GQL values
27
+ @field = entrypoint.field # 1 Pointer
28
+ @context = request.context.to_h # 1 Hash +/- heavy
29
+ @broadcastable = operation.broadcastable? # 1 Boolean
30
+ @created_at = Time.current # 1 Integer
31
+ updated! # 1 Integer
30
32
 
31
- @scope = parse_scope(field.full_scope, request, operation)
32
- @sid = request.schema.subscription_id_for(self)
33
+ @scope = parse_scope(field.full_scope, request, operation) # 1 Integer after save
34
+ @sid = request.schema.subscription_id_for(self) # 1 String
33
35
  end
34
36
 
35
37
  def updated!
36
- @last_updated_at = Time.current
38
+ @updated_at = Time.current
37
39
  end
38
40
 
39
41
  def marshal_dump
@@ -44,7 +46,7 @@ module Rails
44
46
  (+<<~INFO).squish << '>'
45
47
  #<#{self.class.name}
46
48
  #{schema}@#{sid}
47
- [#{scope.nil? ? scope.inspect : scope.hash}, #{args.hash}]
49
+ [#{scope.inspect}, #{args.hash}]
48
50
  #{field.inspect}
49
51
  INFO
50
52
  end