rails-graphql 1.0.0.beta → 1.0.0.rc2

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 (137) 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 +13 -8
  17. data/lib/rails/graphql/alternative/subscription.rb +2 -1
  18. data/lib/rails/graphql/alternative.rb +4 -0
  19. data/lib/rails/graphql/argument.rb +5 -3
  20. data/lib/rails/graphql/callback.rb +10 -8
  21. data/lib/rails/graphql/collectors/hash_collector.rb +12 -1
  22. data/lib/rails/graphql/collectors/json_collector.rb +21 -0
  23. data/lib/rails/graphql/config.rb +86 -59
  24. data/lib/rails/graphql/directive/include_directive.rb +0 -1
  25. data/lib/rails/graphql/directive/skip_directive.rb +0 -1
  26. data/lib/rails/graphql/directive/specified_by_directive.rb +24 -0
  27. data/lib/rails/graphql/directive.rb +31 -25
  28. data/lib/rails/graphql/event.rb +7 -6
  29. data/lib/rails/graphql/field/authorized_field.rb +0 -5
  30. data/lib/rails/graphql/field/input_field.rb +0 -5
  31. data/lib/rails/graphql/field/mutation_field.rb +5 -6
  32. data/lib/rails/graphql/field/output_field.rb +13 -2
  33. data/lib/rails/graphql/field/proxied_field.rb +6 -6
  34. data/lib/rails/graphql/field/resolved_field.rb +1 -1
  35. data/lib/rails/graphql/field/subscription_field.rb +35 -52
  36. data/lib/rails/graphql/field/typed_field.rb +26 -2
  37. data/lib/rails/graphql/field.rb +20 -19
  38. data/lib/rails/graphql/global_id.rb +5 -1
  39. data/lib/rails/graphql/helpers/inherited_collection/array.rb +1 -0
  40. data/lib/rails/graphql/helpers/inherited_collection/base.rb +3 -1
  41. data/lib/rails/graphql/helpers/inherited_collection/hash.rb +2 -1
  42. data/lib/rails/graphql/helpers/registerable.rb +1 -1
  43. data/lib/rails/graphql/helpers/with_arguments.rb +3 -2
  44. data/lib/rails/graphql/helpers/with_assignment.rb +5 -5
  45. data/lib/rails/graphql/helpers/with_callbacks.rb +3 -3
  46. data/lib/rails/graphql/helpers/with_description.rb +10 -8
  47. data/lib/rails/graphql/helpers/with_directives.rb +5 -1
  48. data/lib/rails/graphql/helpers/with_events.rb +1 -0
  49. data/lib/rails/graphql/helpers/with_fields.rb +30 -24
  50. data/lib/rails/graphql/helpers/with_name.rb +3 -2
  51. data/lib/rails/graphql/helpers/with_schema_fields.rb +75 -51
  52. data/lib/rails/graphql/introspection.rb +1 -1
  53. data/lib/rails/graphql/railtie.rb +3 -2
  54. data/lib/rails/graphql/railties/app/base_channel.rb +10 -0
  55. data/lib/rails/graphql/railties/app/base_controller.rb +12 -0
  56. data/lib/rails/graphql/railties/app/views/_cable.js.erb +56 -0
  57. data/lib/rails/graphql/railties/app/views/_fetch.js.erb +20 -0
  58. data/lib/rails/graphql/railties/app/views/graphiql.html.erb +101 -0
  59. data/lib/rails/graphql/railties/base_generator.rb +3 -9
  60. data/lib/rails/graphql/railties/channel.rb +8 -8
  61. data/lib/rails/graphql/railties/controller.rb +51 -26
  62. data/lib/rails/graphql/request/arguments.rb +2 -1
  63. data/lib/rails/graphql/request/backtrace.rb +31 -10
  64. data/lib/rails/graphql/request/component/field.rb +15 -8
  65. data/lib/rails/graphql/request/component/fragment.rb +13 -7
  66. data/lib/rails/graphql/request/component/operation/subscription.rb +4 -6
  67. data/lib/rails/graphql/request/component/operation.rb +12 -5
  68. data/lib/rails/graphql/request/component/spread.rb +13 -4
  69. data/lib/rails/graphql/request/component/typename.rb +1 -1
  70. data/lib/rails/graphql/request/component.rb +2 -0
  71. data/lib/rails/graphql/request/context.rb +1 -1
  72. data/lib/rails/graphql/request/event.rb +6 -2
  73. data/lib/rails/graphql/request/helpers/directives.rb +1 -0
  74. data/lib/rails/graphql/request/helpers/selection_set.rb +10 -4
  75. data/lib/rails/graphql/request/helpers/value_writers.rb +8 -5
  76. data/lib/rails/graphql/request/prepared_data.rb +3 -1
  77. data/lib/rails/graphql/request/steps/organizable.rb +1 -1
  78. data/lib/rails/graphql/request/steps/preparable.rb +1 -1
  79. data/lib/rails/graphql/request/steps/resolvable.rb +1 -1
  80. data/lib/rails/graphql/request/strategy/sequenced_strategy.rb +3 -3
  81. data/lib/rails/graphql/request/strategy.rb +18 -4
  82. data/lib/rails/graphql/request/subscription.rb +18 -16
  83. data/lib/rails/graphql/request.rb +71 -41
  84. data/lib/rails/graphql/schema.rb +39 -86
  85. data/lib/rails/graphql/shortcuts.rb +11 -5
  86. data/lib/rails/graphql/source/active_record/builders.rb +22 -24
  87. data/lib/rails/graphql/source/active_record_source.rb +96 -34
  88. data/lib/rails/graphql/source/base.rb +13 -40
  89. data/lib/rails/graphql/source/builder.rb +14 -22
  90. data/lib/rails/graphql/source/scoped_arguments.rb +10 -4
  91. data/lib/rails/graphql/source.rb +31 -38
  92. data/lib/rails/graphql/subscription/provider/action_cable.rb +10 -9
  93. data/lib/rails/graphql/subscription/provider/base.rb +6 -5
  94. data/lib/rails/graphql/subscription/store/base.rb +5 -9
  95. data/lib/rails/graphql/subscription/store/memory.rb +18 -9
  96. data/lib/rails/graphql/type/creator.rb +198 -0
  97. data/lib/rails/graphql/type/enum.rb +17 -9
  98. data/lib/rails/graphql/type/input.rb +30 -7
  99. data/lib/rails/graphql/type/interface.rb +15 -4
  100. data/lib/rails/graphql/type/object/directive_object.rb +6 -5
  101. data/lib/rails/graphql/type/object/input_value_object.rb +3 -4
  102. data/lib/rails/graphql/type/object/type_object.rb +40 -13
  103. data/lib/rails/graphql/type/object.rb +11 -6
  104. data/lib/rails/graphql/type/scalar/binary_scalar.rb +2 -0
  105. data/lib/rails/graphql/type/scalar/date_scalar.rb +2 -0
  106. data/lib/rails/graphql/type/scalar/date_time_scalar.rb +2 -0
  107. data/lib/rails/graphql/type/scalar/decimal_scalar.rb +2 -0
  108. data/lib/rails/graphql/type/scalar/json_scalar.rb +3 -1
  109. data/lib/rails/graphql/type/scalar/time_scalar.rb +3 -1
  110. data/lib/rails/graphql/type/scalar.rb +2 -2
  111. data/lib/rails/graphql/type/union.rb +7 -2
  112. data/lib/rails/graphql/type.rb +10 -2
  113. data/lib/rails/graphql/type_map.rb +20 -7
  114. data/lib/rails/graphql/uri.rb +5 -4
  115. data/lib/rails/graphql/version.rb +6 -2
  116. data/lib/rails/graphql.rb +11 -8
  117. data/test/assets/introspection-mem.txt +1 -1
  118. data/test/assets/introspection.gql +2 -0
  119. data/test/assets/mem.gql +74 -60
  120. data/test/assets/mysql.gql +69 -55
  121. data/test/assets/sqlite.gql +78 -64
  122. data/test/assets/translate.gql +50 -39
  123. data/test/config.rb +2 -1
  124. data/test/graphql/schema_test.rb +2 -31
  125. data/test/graphql/source_test.rb +1 -11
  126. data/test/graphql/type/interface_test.rb +8 -5
  127. data/test/graphql/type/object_test.rb +8 -2
  128. data/test/graphql/type_map_test.rb +13 -16
  129. data/test/integration/global_id_test.rb +4 -4
  130. data/test/integration/memory/star_wars_validation_test.rb +2 -2
  131. data/test/integration/mysql/star_wars_introspection_test.rb +1 -1
  132. data/test/integration/resolver_precedence_test.rb +1 -1
  133. data/test/integration/schemas/memory.rb +3 -4
  134. data/test/integration/sqlite/star_wars_global_id_test.rb +27 -21
  135. data/test/integration/sqlite/star_wars_introspection_test.rb +1 -1
  136. data/test/integration/translate_test.rb +26 -14
  137. metadata +22 -9
@@ -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
@@ -18,7 +18,7 @@ module Rails
18
18
 
19
19
  # Helper method to initialize an operation given the node
20
20
  def build(request, node)
21
- request.build(const_get(node.type.to_s.classify), request, node)
21
+ request.build(const_get(node.type.to_s.classify, false), request, node)
22
22
  end
23
23
 
24
24
  # Rewrite the kind to always return +:operation+
@@ -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