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
@@ -8,7 +8,7 @@ module Rails
8
8
  # source object, creating:
9
9
  # 1. 1 Object
10
10
  # 2. 1 Input
11
- # 3. 2 Query fields (ingular and plural)
11
+ # 3. 2 Query fields (singular and plural)
12
12
  # 4. 3 Mutation fields (create, update, destroy)
13
13
  class Source::ActiveRecordSource < Source::Base
14
14
  include Source::ScopedArguments
@@ -24,17 +24,25 @@ module Rails
24
24
  # associations associated to the object
25
25
  class_attribute :with_associations, instance_accessor: false, default: true
26
26
 
27
+ # Set what type of errors should be exported to the extensions of the
28
+ # request when trying to save records. False will disable it
29
+ class_attribute :errors_to_extensions, instance_accessor: false, default: false
30
+
31
+ # Marks if the source should be threated as an interface, meaning that
32
+ # no object will be created, instead an interface will
33
+ class_attribute :act_as_interface, instance_accessor: false, default: false
34
+
27
35
  # The name of the class (or the class itself) to be used as superclass for
28
36
  # the generate GraphQL interface type of this source
29
37
  class_attribute :interface_class, instance_accessor: false
30
38
 
31
39
  %i[object interface input].each do |type|
32
- settings = { abstract: true, with_owner: true }
40
+ settings = { abstract: true, owner: true }
33
41
  send("#{type}_class=", create_type(type, **settings))
34
42
  end
35
43
 
36
44
  self.abstract = true
37
- self.hook_names = hook_names.to_a.insert(1, :enums).to_set
45
+ self.hook_names = hook_names.to_a.insert(1, :enums, :interface).to_set.freeze
38
46
 
39
47
  delegate :primary_key, :singular, :plural, :model, :id_columns, to: :class
40
48
 
@@ -48,44 +56,51 @@ module Rails
48
56
  build_reflection_fields(self)
49
57
  end
50
58
 
59
+ step(:interface) do
60
+ build_attribute_fields(self)
61
+ build_reflection_fields(self)
62
+ end
63
+
51
64
  step(:input) do
52
65
  extra = GraphQL.enumerate(primary_key).entries.product([{ null: true }]).to_h
53
66
  build_attribute_fields(self, **extra)
54
67
  build_reflection_inputs(self)
55
68
 
56
- safe_field(model.inheritance_column, :string, null: false) if object.interface?
69
+ safe_field(model.inheritance_column, :string, null: false) if interface?
57
70
  safe_field(:_delete, :boolean, default: false)
58
71
 
59
72
  reference = model.new
60
73
  model.columns_hash.each_value do |column|
61
74
  change_field(column.name, default: reference[column.name]) \
62
- if column.default.present? && field?(column.name)
75
+ if column.default.present? && has_field?(column.name)
63
76
  end
64
77
  end
65
78
 
66
79
  step(:query) do
67
- build_object
80
+ interface? ? build_interface : build_object
81
+ type = interface? ? interface : object
68
82
 
69
- safe_field(plural, object, full: true) do
83
+ safe_field(plural, type, full: true) do
70
84
  before_resolve(:load_records)
71
85
  end
72
86
 
73
- safe_field(singular, object, null: false) do
87
+ safe_field(singular, type, null: false) do
74
88
  build_primary_key_arguments(self)
75
89
  before_resolve(:load_record)
76
90
  end
77
91
  end
78
92
 
79
93
  step(:mutation) do
80
- build_object
94
+ interface? ? build_interface : build_object
95
+ type = interface? ? interface : object
81
96
  build_input
82
97
 
83
- safe_field("create_#{singular}", object, null: false) do
98
+ safe_field("create_#{singular}", type, null: false) do
84
99
  argument(singular, input, null: false)
85
100
  perform(:create_record)
86
101
  end
87
102
 
88
- safe_field("update_#{singular}", object, null: false) do
103
+ safe_field("update_#{singular}", type, null: false) do
89
104
  build_primary_key_arguments(self)
90
105
  argument(singular, input, null: false)
91
106
  before_resolve(:load_record)
@@ -124,6 +139,22 @@ module Rails
124
139
  super if model&.table_exists?
125
140
  end
126
141
 
142
+ # Allows setting up an interface instead of an object. Mostly because
143
+ # some models are better dealt as interfaces than actual objects
144
+ def interface
145
+ @interface ||= create_type(superclass: interface_class, gql_name: object_name)
146
+ end
147
+
148
+ # Checks if the source is building an interface instead of an object
149
+ def interface?
150
+ defined?(@interface) || act_as_interface? || sti_interface?
151
+ end
152
+
153
+ # Provides access to the default plural query field, for associations interconnection
154
+ def collection_field
155
+ find_field(:query, plural)
156
+ end
157
+
127
158
  # Hook into the unregister to clean enums
128
159
  def unregister!
129
160
  super
@@ -151,23 +182,56 @@ module Rails
151
182
 
152
183
  private
153
184
 
185
+ # Hook into the build process to selective avoid :interface or :object
186
+ def build!(type)
187
+ return if type == :object && interface?
188
+ return if type == :interface && !interface?
189
+ super
190
+ end
191
+
154
192
  def presence_validator
155
193
  ::ActiveRecord::Validations::PresenceValidator
156
194
  end
157
195
  end
158
196
 
159
- # Prepare to load multiple records from the underlying table
160
- def load_records(scope = model.default_scoped)
197
+ # Prepare to load multiple records from the underlying model
198
+ def load_records(scope = nil)
199
+ scope ||= event.last_result || model.default_scoped
161
200
  inject_scopes(scope, :relation)
162
201
  end
163
202
 
164
- # Prepare to load a single record from the underlying table
165
- def load_record(scope = model.default_scoped, find_by: nil)
203
+ # Prepare to load a single record from the underlying model
204
+ def load_record(scope = nil, find_by: nil)
205
+ scope ||= event.last_result || model.default_scoped
166
206
  find_by ||= { primary_key => event.argument(primary_key) }
167
207
  inject_scopes(scope, :relation).find_by(find_by)
168
208
  end
169
209
 
170
- # Get the chain result and preload the records with thre resulting scope
210
+ # The perform step for the +create+ based mutation
211
+ def create_record
212
+ input_argument.resource.tap(&:save!)
213
+ rescue ::ActiveRecord::RecordInvalid => error
214
+ errors_to_extensions(error.record.errors)
215
+ raise
216
+ end
217
+
218
+ # The perform step for the +update+ based mutation
219
+ def update_record
220
+ current_value.tap { |record| record.update!(**input_argument.params) }
221
+ rescue ::ActiveRecord::RecordInvalid => error
222
+ errors_to_extensions(error.record.errors)
223
+ raise
224
+ end
225
+
226
+ # The perform step for the +delete+ based mutation
227
+ def destroy_record
228
+ !!current_value.destroy!
229
+ rescue ::ActiveRecord::RecordInvalid => error
230
+ errors_to_extensions(error.record.errors)
231
+ raise
232
+ end
233
+
234
+ # Get the chain result and preload the records with the resulting scope
171
235
  def preload_association(association, scope = nil)
172
236
  event.stop(preload(association, scope || event.last_result), layer: :object)
173
237
  end
@@ -177,10 +241,11 @@ module Rails
177
241
  scope = model._reflect_on_association(association).klass.default_scoped
178
242
 
179
243
  # Apply proxied injected scopes
180
- proxied = event.field.try(:proxied_owner)
181
- scope = event.on_instance(proxied) do |instance|
182
- instance.inject_scopes(scope, :relation)
183
- end if proxied.present? && proxied <= Source::ActiveRecordSource
244
+ # TODO: Arguments comes with their proxy, so we might not need this
245
+ # proxied = event.field.try(:proxied_owner)
246
+ # scope = event.on_instance(proxied) do |instance|
247
+ # instance.inject_scopes(scope, :relation)
248
+ # end if proxied.present? && proxied <= Source::ActiveRecordSource
184
249
 
185
250
  # Apply self defined injected scopes
186
251
  inject_scopes(scope, :relation)
@@ -189,26 +254,21 @@ module Rails
189
254
  # Once the records are pre-loaded due to +preload_association+, use the
190
255
  # parent value and the preloader result to get the records
191
256
  def parent_owned_records(collection_result = false)
192
- data = event.data[:prepared]
257
+ data = event.data[:prepared_data]
193
258
  return collection_result ? [] : nil unless data
194
259
 
195
260
  result = data.records_by_owner[current_value] || []
196
261
  collection_result ? result : result.first
197
262
  end
198
263
 
199
- # The perform step for the +create+ based mutation
200
- def create_record
201
- input_argument.resource.tap(&:save!)
202
- end
203
-
204
- # The perform step for the +update+ based mutation
205
- def update_record
206
- current_value.tap { |record| record.update!(**input_argument.params) }
207
- end
264
+ # Expose the errors to the extensions of the response
265
+ def errors_to_extensions(errors, path = nil, format = nil)
266
+ format ||= self.class.errors_to_extensions
267
+ return unless format
208
268
 
209
- # The perform step for the +delete+ based mutation
210
- def destroy_record
211
- !!current_value.destroy!
269
+ path ||= [operation.name, field.gql_name].compact
270
+ hash = GraphQL.enumerate(path).reduce(request.extensions) { |h, k| h[k] ||= {} }
271
+ hash.replace(format == :messages ? errors.as_json : errors.details)
212
272
  end
213
273
 
214
274
  protected
@@ -32,8 +32,8 @@ module Rails
32
32
 
33
33
  # Unregister all objects that this source was providing
34
34
  def unregister!
35
- GraphQL.type_map.unregister(*created_types) if defined?(@created_types)
36
35
  @object = @input = nil
36
+ super
37
37
  end
38
38
 
39
39
  # Return the GraphQL object type associated with the source. It will
@@ -59,52 +59,24 @@ module Rails
59
59
  enumerator = values.each_pair if values.respond_to?(:each_pair)
60
60
  enumerator ||= values.each.with_index
61
61
 
62
- xargs = xargs.reverse_merge(once: true)
63
- create_type(:enum, as: enum_name.classify, **xargs) do
64
- indexed! if enumerator.first.last.is_a?(Numeric)
65
- enumerator.sort_by(&:last).map(&:first).each(&method(:add))
66
- instance_exec(&block) if block.present?
67
- end
62
+ xargs[:values] = enumerator.sort_by(&:last).map(&:first)
63
+ xargs[:indexed] = enumerator.first.last.is_a?(Numeric)
64
+
65
+ create_type(:enum, enum_name.classify, **xargs, &block)
68
66
  end
69
67
 
70
68
  # Helper method to create a class based on the given +type+ and
71
69
  # allows several other settings to be executed on it
72
- def create_type(type = nil, **xargs, &block)
73
- name = "#{gql_module.name}::#{xargs.delete(:as) || base_name}"
74
- superclass = xargs.delete(:superclass)
75
- with_owner = xargs.delete(:with_owner)
76
-
77
- if superclass.nil?
78
- superclass = type.to_s.classify
79
- elsif superclass.is_a?(String)
80
- superclass = superclass.constantize
81
- end
82
-
83
- source = self
84
- gql_name = xargs.delete(:gql_name)
85
- Schema.send(:create_type, name, superclass, **xargs) do
86
- include Helpers::WithOwner if with_owner
87
- set_namespaces(*source.namespaces)
88
-
89
- instance_variable_set(:@gql_name, gql_name) unless gql_name.nil?
90
-
91
- self.owner = source if respond_to?(:owner=)
92
- self.assigned_to = source.safe_assigned_class \
93
- if source.assigned? && is_a?(Helpers::WithAssignment)
94
-
95
- instance_exec(&block) if block.present?
96
- end.tap { |klass| created_types << klass }
97
- end
70
+ def create_type(type = nil, name = base_name, **xargs, &block)
71
+ xargs[:owner] ||= self
72
+ xargs[:namespaces] = namespaces
73
+ xargs[:assigned_to] = safe_assigned_class
74
+ superclass = xargs.delete(:superclass) || type
98
75
 
99
- private
100
-
101
- # Keep track of all the types created byt this source
102
- def created_types
103
- @created_types ||= []
76
+ GraphQL::Type.create!(self, name, superclass, **xargs, &block)
104
77
  end
105
78
 
106
79
  end
107
-
108
80
  end
109
81
  end
110
82
  end
@@ -34,10 +34,10 @@ module Rails
34
34
  type = method_name.to_s[6..-1]
35
35
  type = type.singularize unless hook_names.include?(type.to_sym)
36
36
  type = type.to_sym
37
+ return if built?(type)
37
38
 
38
39
  import_skips_for(type, xargs)
39
-
40
- build!(type, *args, **xargs, &block) unless built?(type)
40
+ build!(type, *args, **xargs, &block)
41
41
  end
42
42
 
43
43
  protected
@@ -47,13 +47,6 @@ module Rails
47
47
  @built ||= Set.new
48
48
  end
49
49
 
50
- # Make sure to mark the hook name as built
51
- def run_hooks(hook_name, *)
52
- super
53
- ensure
54
- built << hook_name
55
- end
56
-
57
50
  private
58
51
 
59
52
  # Import all options-based settings for skipping field
@@ -100,29 +93,23 @@ module Rails
100
93
  # Build all the objects associated with this source
101
94
  def build!(type)
102
95
  ensure_build!(type)
96
+ built << type
103
97
 
98
+ schema_type = Helpers::WithSchemaFields::TYPE_FIELD_CLASS.key?(type)
104
99
  catch(:skip) { run_hooks(:start) } unless built?(:start)
105
- catch(:skip) { run_hooks(type, hook_scope_for(type)) }
100
+ catch(:skip) { run_hooks(type, hook_scope_for(type, schema_type)) }
106
101
 
107
- built << type.to_sym
102
+ attach_fields!(type, fields_for(type)) if schema_type && fields_for?(type)
108
103
  end
109
104
 
110
105
  # Get the correct +self_object+ for the hook instance
111
- def hook_scope_for(type)
106
+ def hook_scope_for(type, schema_type)
112
107
  type = type.to_sym
113
- object =
114
- if Helpers::WithSchemaFields::TYPE_FIELD_CLASS.key?(type)
115
- Helpers::WithSchemaFields::ScopedConfig.new(self, type)
116
- else
117
- Helpers::AttributeDelegator.new(self, type)
118
- end
119
-
120
- Source::ScopedConfig.new(self, object, type)
108
+ klass = schema_type ? 'WithSchemaFields::ScopedConfig' : 'AttributeDelegator'
109
+ Source::ScopedConfig.new(self, Helpers.const_get(klass).new(self, type), type)
121
110
  end
122
111
 
123
112
  end
124
113
  end
125
114
  end
126
115
  end
127
-
128
-
@@ -15,8 +15,8 @@ module Rails
15
15
  def initialize(*args, block:, on: nil, **xargs)
16
16
  super(*args, **xargs)
17
17
 
18
- @block = block
19
- @fields = GraphQL.enumerate(on)
18
+ @block = (block == true) ? name : block
19
+ @fields = on
20
20
  end
21
21
 
22
22
  # Apply the argument block to the given object, using or not the value
@@ -36,7 +36,7 @@ module Rails
36
36
  def attach_to?(field)
37
37
  return true if @fields.nil?
38
38
 
39
- @fields.any? do |item|
39
+ GraphQL.enumerate(@fields).any? do |item|
40
40
  (item.is_a?(Symbol) && field.name.eql?(item)) || field.gql_name.eql?(item)
41
41
  end
42
42
  end
@@ -48,11 +48,17 @@ module Rails
48
48
  defined?(@scoped_arguments) ? @scoped_arguments : {}
49
49
  end
50
50
 
51
+ # Hook into the attach fields process to attach the scoped arguments
52
+ def attach_fields!(type, fields)
53
+ attach_scoped_arguments_to(fields.values)
54
+ super
55
+ end
56
+
51
57
  protected
52
58
 
53
59
  # Add a new scoped param to the list
54
60
  def scoped_argument(param, type = :string, proc_method = nil, **settings, &block)
55
- block = proc_method if proc_method.present? && block.nil?
61
+ block ||= proc_method if proc_method.present?
56
62
  argument = Argument.new(param, type, **settings, owner: self, block: block)
57
63
  (@scoped_arguments ||= {})[argument.name] = argument
58
64
  end
@@ -17,13 +17,6 @@ module Rails
17
17
 
18
18
  include Helpers::Instantiable
19
19
 
20
- ATTACH_FIELDS_STEP = -> do
21
- if fields?
22
- attach_fields!(type, fields)
23
- attach_scoped_arguments_to(fields.values)
24
- end
25
- end
26
-
27
20
  autoload :Base
28
21
  autoload :Builder
29
22
 
@@ -39,7 +32,6 @@ module Rails
39
32
  self_object.safe_field(name, *args, **xargs, &block)
40
33
  end
41
34
 
42
- # skip_field?(item.name, on: holder.kind)
43
35
  def respond_to_missing?(method_name, include_private = false)
44
36
  self_object.respond_to?(method_name, include_private) ||
45
37
  receiver.respond_to?(method_name, include_private)
@@ -61,7 +53,7 @@ module Rails
61
53
  # set the order of the execution of the hooks while validating the hooks
62
54
  # callbacks using the +on+ method
63
55
  class_attribute :hook_names, instance_accessor: false,
64
- default: %i[start object input query mutation subscription].to_set
56
+ default: %i[start object input query mutation subscription].to_set.freeze
65
57
 
66
58
  # The list of hooks defined in order to describe a source
67
59
  inherited_collection :hooks, instance_reader: false, type: :hash_array
@@ -113,6 +105,22 @@ module Rails
113
105
  base_sources.reverse_each.find { |source| object <= source.assigned_class }
114
106
  end
115
107
 
108
+ # Add a new description hook. You can use +throw :skip+ and skip
109
+ # parent hooks. If the class is already built, then execute the hook.
110
+ # Use the +unshift: true+ to add the hook at the beginning of the
111
+ # list, which will then be the last to run
112
+ def step(hook_name, unshift: false, &block)
113
+ raise ArgumentError, (+<<~MSG).squish unless hook_names.include?(hook_name.to_sym)
114
+ The #{hook_name.inspect} is not a valid hook method.
115
+ MSG
116
+
117
+ if built?(hook_name)
118
+ hook_scope_for(hook_name).instance_exec(&block)
119
+ else
120
+ hooks[hook_name.to_sym].public_send(unshift ? :unshift : :push, block)
121
+ end
122
+ end
123
+
116
124
  # Attach all defined schema fields into the schemas using the namespaces
117
125
  # configured for the source
118
126
  def attach_fields!(type = :all, from = self)
@@ -149,22 +157,6 @@ module Rails
149
157
  segmented_skip_fields[source] += fields.flatten.compact.map(&:to_sym).to_set
150
158
  end
151
159
 
152
- # Add a new description hook. You can use +throw :skip+ and skip
153
- # parent hooks. If the class is already built, then execute the hook.
154
- # Use the +unshift: true+ to add the hook at the beginning of the
155
- # list, which will then be the last to run
156
- def step(hook_name, unshift: false, &block)
157
- raise ArgumentError, (+<<~MSG).squish unless hook_names.include?(hook_name.to_sym)
158
- The #{hook_name.inspect} is not a valid hook method.
159
- MSG
160
-
161
- if built?(hook_name)
162
- hook_scope_for(hook_name).instance_exec(&block)
163
- else
164
- hooks[hook_name.to_sym].public_send(unshift ? :unshift : :push, block)
165
- end
166
- end
167
-
168
160
  # Creates a hook that throws a done action, preventing any parent hooks
169
161
  def skip(*names)
170
162
  names.each do |hook_name|
@@ -183,22 +175,21 @@ module Rails
183
175
  # It's an alternative to +self.hook_names -= %i[*names]+ which
184
176
  # disables a specific hook
185
177
  def disable(*names)
186
- self.hook_names -= names.flatten.map do |hook_name|
178
+ list = names.flatten.map do |hook_name|
187
179
  hook_name.to_s.singularize.to_sym
188
180
  end
181
+
182
+ self.hook_names = (hook_names - list).freeze
189
183
  end
190
184
 
191
185
  # It's an alternative to +self.hook_names += %i[*names]+ which
192
186
  # enables additional hooks
193
187
  def enable(*names)
194
- self.hook_names += names.flatten.map do |hook_name|
188
+ list = names.flatten.map do |hook_name|
195
189
  hook_name.to_s.singularize.to_sym
196
190
  end
197
- end
198
191
 
199
- # Return the module where the GraphQL types should be created at
200
- def gql_module
201
- name.start_with?('GraphQL::') ? module_parent : ::GraphQL
192
+ self.hook_names = (hook_names + list).freeze
202
193
  end
203
194
 
204
195
  # Add one or more fields to the list of fields that needs to be
@@ -236,17 +227,12 @@ module Rails
236
227
  super if defined? super
237
228
  end
238
229
 
239
- # Find all classes that inherits from source that are abstract,
240
- # meaning that they are a base sources
230
+ # Constantize all the base sources that were defined in the settings
241
231
  def base_sources
242
232
  @@base_sources ||= GraphQL.config.sources.map(&:constantize).to_set
243
233
  end
244
234
 
245
235
  end
246
-
247
- step(:query, &ATTACH_FIELDS_STEP)
248
- step(:mutation, &ATTACH_FIELDS_STEP)
249
- step(:subscription, &ATTACH_FIELDS_STEP)
250
236
  end
251
237
  end
252
238
  end
@@ -10,6 +10,7 @@ module Rails
10
10
  #
11
11
  # The subscription provider associated with Rails Action Cable, that
12
12
  # delivers subscription notifications through an Action Cable Channel
13
+ # TODO: Try to serialize and deserialize the origin
13
14
  class ActionCable < Base
14
15
  INTERNAL_CHANNEL = 'rails-graphql:events'
15
16
 
@@ -17,7 +18,7 @@ module Rails
17
18
 
18
19
  def initialize(*args, **options)
19
20
  @cable = options.fetch(:cable, ::ActionCable)
20
- @prefix = options.fetch(:prefix, 'graphql')
21
+ @prefix = options.fetch(:prefix, 'rails-graphql')
21
22
 
22
23
  @event_callback = ->(message) do
23
24
  method_name, args, xargs = Marshal.load(message)
@@ -46,20 +47,20 @@ module Rails
46
47
  end
47
48
 
48
49
  def async_remove(item)
49
- return unless instance?(item) || !(item = store.fetch(item)).nil?
50
+ return if (item = store.fetch(item)).nil?
50
51
  cable.server.broadcast(stream_name(item), unsubscribed_payload)
51
52
  store.remove(item)
52
53
 
53
54
  log(:removed, item)
54
55
  end
55
56
 
56
- def async_update(item, data = nil)
57
- return unless instance?(item) || !(item = store.fetch(item)).nil?
57
+ def async_update(item, data = nil, **xargs)
58
+ return if (item = store.fetch(item)).nil?
58
59
  removing = false
59
60
 
60
61
  log(:updated, item) do
61
- data = execute(item) if data.nil?
62
- item.updated!
62
+ data = execute(item, **xargs) if data.nil?
63
+ store.update!(item)
63
64
 
64
65
  unless (removing = unsubscribing?(data))
65
66
  data = { 'result' => data, 'more' => true }
@@ -71,7 +72,7 @@ module Rails
71
72
  end
72
73
 
73
74
  def stream_name(item)
74
- "#{prefix}:#{instance?(item) ? item.sid : item}"
75
+ "#{prefix}:#{item.sid}"
75
76
  end
76
77
 
77
78
  protected
@@ -80,8 +81,8 @@ module Rails
80
81
  item.origin.stream_from(stream_name(item))
81
82
  end
82
83
 
83
- def execute(subscription, **xargs)
84
- super(subscription, origin: subscription.origin, **xargs, as: :hash)
84
+ def execute(item, **xargs)
85
+ super(item, origin: item.origin, **xargs, as: :hash)
85
86
  end
86
87
 
87
88
  def async_exec(method_name, *args, **xargs)
@@ -94,7 +94,7 @@ module Rails
94
94
  # Update one single subscription, for broadcasting, use +update_all+
95
95
  # or +search_and_update+. You can provide the +data+ that will be sent
96
96
  # to upstream, skipping it from being collected from a request
97
- async_exec def update(item, data = nil)
97
+ async_exec def update(item, data = nil, **xargs)
98
98
  raise NotImplementedError, +"#{self.class.name} does not implement update"
99
99
  end
100
100
 
@@ -104,15 +104,15 @@ module Rails
104
104
  end
105
105
 
106
106
  # A simple shortcut for calling update on each individual sid
107
- async_exec def update_all(*sids)
107
+ async_exec def update_all(*sids, **xargs)
108
108
  return if sids.blank?
109
109
 
110
110
  enum = GraphQL.enumerate(store.fetch(*sids))
111
111
  enum.group_by(&:operation_id).each_value do |subscriptions|
112
- data = execute(subscriptions.first, broadcasting: true) \
112
+ data = execute(subscriptions.first, **xargs, broadcasting: true) \
113
113
  unless subscriptions.one? || first.broadcastable?
114
114
 
115
- subscriptions.each { |item| update(item, data) }
115
+ subscriptions.each { |item| update(item, data, **xargs) }
116
116
  end
117
117
  end
118
118
 
@@ -123,7 +123,8 @@ module Rails
123
123
 
124
124
  # A shortcut for finding the subscriptions and then updating them
125
125
  async_exec def search_and_update(**options)
126
- update_all(*find_each(**options))
126
+ xargs = options.slice(:data_for)
127
+ update_all(*find_each(**options), **xargs)
127
128
  end
128
129
 
129
130
  # Get the payload that should be used when unsubscribing
@@ -65,6 +65,11 @@ module Rails
65
65
  raise NotImplementedError, +"#{self.class.name} does not implement remove"
66
66
  end
67
67
 
68
+ # Marks that a subscription has received an update
69
+ def update!(item)
70
+ raise NotImplementedError, +"#{self.class.name} does not implement update!"
71
+ end
72
+
68
73
  # Check if a given sid or instance is stored
69
74
  def has?(item)
70
75
  raise NotImplementedError, +"#{self.class.name} does not implement has?"
@@ -123,21 +128,12 @@ module Rails
123
128
  def hash_for(value, klass = nil)
124
129
  if !klass.nil?
125
130
  klass.hash ^ value.hash
126
- elsif extract_class_from?(value)
127
- value.class.hash ^ value.id.hash
128
131
  elsif value.is_a?(Numeric)
129
132
  value
130
133
  else
131
134
  value.hash
132
135
  end
133
136
  end
134
-
135
- # Check if ActiveRecord::Base is available and then if the object
136
- # provided is an instance of it, so that the serialize can work
137
- # correctly
138
- def extract_class_from?(value)
139
- defined?(ActiveRecord) && value.is_a?(ActiveRecord::Base)
140
- end
141
137
  end
142
138
  end
143
139
  end