rails-graphql 1.0.0.beta → 1.0.0.rc1

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 (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