graphql-activerecord 0.9.1 → 0.10.0.pre.alpha1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b747cb0c89283eb53f5bed955aba5ac63c67e403
4
- data.tar.gz: 6fc9db07f08a93526bc4d2d6cae816efd4ecf0d5
3
+ metadata.gz: 492bdd93511565f485130b3faa32d9af36eed176
4
+ data.tar.gz: bafa803ca1452a1f7ceefe66362cf4c063e06a49
5
5
  SHA512:
6
- metadata.gz: c9724e12c8b19c2361183aaf25e2823e19c262c0a36ff27d038bc4d7a8972fb63ccfc1104a5e7c3eac450bf1e4a680e0d57daa1718c52ecbaef6b5b71ddb11fa
7
- data.tar.gz: 32d5a9dbfed54b57b8e201299294a5413c6958f623c1f83ec8782f6668f26b5967876194097699b7a144d3d0ee88aa8959ef99addcc5aaf2eb0e14533babf1a2
6
+ metadata.gz: 3e5652bd12e1b46d9bb1834aa8eebe149f5e484d79d29fa7d25297121e626f0962729199dc135e532d348344f4105ca35b3f2b6be5d5c3c02aaca938f0b37edf
7
+ data.tar.gz: ada3b96ec9bcdbfbdf0dee07ed79ccce4b603a7ac353a99f0b2acc65cec877f081d496e4e39a688f6d1e2a27bec5696424fd1ae6eb25efca36fefc1157c895a5
@@ -19,7 +19,8 @@ require 'graphql/models/relation_loader'
19
19
  # Order matters...
20
20
  require 'graphql/models/promise_relation_connection'
21
21
  require 'graphql/models/relation_load_request'
22
- require 'graphql/models/scalar_types'
22
+ require 'graphql/models/database_types'
23
+ require 'graphql/models/reflection'
23
24
  require 'graphql/models/definition_helpers'
24
25
  require 'graphql/models/definition_helpers/associations'
25
26
  require 'graphql/models/definition_helpers/attributes'
@@ -30,16 +31,14 @@ require 'graphql/models/mutation_helpers/validation_error'
30
31
  require 'graphql/models/mutation_helpers/validation'
31
32
  require 'graphql/models/mutation_field_map'
32
33
 
33
- require 'graphql/models/proxy_block'
34
34
  require 'graphql/models/backed_by_model'
35
- require 'graphql/models/object_type'
36
35
  require 'graphql/models/mutator'
37
36
 
38
37
 
39
38
  module GraphQL
40
39
  module Models
41
40
  class << self
42
- attr_accessor :node_interface_proc, :model_from_id, :authorize, :id_for_model
41
+ attr_accessor :model_from_id, :authorize, :id_for_model, :model_to_graphql_type
43
42
  end
44
43
 
45
44
  # Returns a promise that will traverse the associations and resolve to the model at the end of the path.
@@ -82,5 +81,30 @@ module GraphQL
82
81
  MutationHelpers.print_input_fields(mutator_definition.field_map, definer, "#{prefix}Input")
83
82
  mutator_definition
84
83
  end
84
+
85
+ def self.get_graphql_type(model_class)
86
+ model_class = model_class.constantize if model_class.is_a?(String)
87
+
88
+ if model_to_graphql_type
89
+ model_to_graphql_type[model_class]
90
+ else
91
+ "#{model_class.name}Type".safe_constantize
92
+ end
93
+ end
94
+
95
+ def self.get_graphql_type!(model_class)
96
+ type = get_graphql_type(model_class)
97
+ fail RuntimeError, "Could not locate GraphQL type for model #{model_class}" if type.nil?
98
+ type
99
+ end
85
100
  end
86
101
  end
102
+
103
+ GraphQL::ObjectType.accepts_definitions(
104
+ backed_by_model: -> (graph_type, model_type, &block) do
105
+ model_type = model_type.to_s.classify.constantize unless model_type.is_a?(Class)
106
+
107
+ backer = GraphQL::Models::BackedByModel.new(graph_type, model_type)
108
+ backer.instance_exec(&block)
109
+ end
110
+ )
@@ -41,7 +41,7 @@ module GraphQL
41
41
  if defined_enums.include?(attribute.to_s)
42
42
  values = defined_enums[attribute.to_s].keys
43
43
  else
44
- values = DefinitionHelpers.detect_inclusion_values(self, attribute)
44
+ values = Reflection.possible_values(self, attribute)
45
45
  end
46
46
 
47
47
  if values.nil?
@@ -1,14 +1,18 @@
1
1
  module GraphQL
2
2
  module Models
3
3
  class BackedByModel
4
- attr_accessor :graph_type, :model_type, :object_to_model
5
-
6
4
  DEFAULT_OBJECT_TO_MODEL = -> (obj) { obj }
7
5
 
8
- def initialize(graph_type, model_type)
6
+ def initialize(graph_type, model_type, base_model_type: model_type, path: [], object_to_model: DEFAULT_OBJECT_TO_MODEL, detect_nulls: true)
7
+ model_type = model_type.to_s.classify.constantize unless model_type.is_a?(Class)
8
+ base_model_type = base_model_type.to_s.classify.constantize unless model_type.is_a?(Class)
9
+
9
10
  @graph_type = graph_type
10
11
  @model_type = model_type
11
- @object_to_model = DEFAULT_OBJECT_TO_MODEL
12
+ @object_to_model = object_to_model
13
+ @base_model_type = base_model_type
14
+ @path = path
15
+ @detect_nulls = detect_nulls
12
16
  end
13
17
 
14
18
  def types
@@ -20,36 +24,90 @@ module GraphQL
20
24
  @object_to_model
21
25
  end
22
26
 
23
- def attr(name, **options, &block)
24
- DefinitionHelpers.define_attribute(graph_type, model_type, model_type, [], name, object_to_model, options, &block)
27
+ # Allows you to overide the automatic nullability detection. By default, nulls are detected. However, attributes inside
28
+ # of a proxy_to block are assumed to be nullable, unless the association itself has a presence validator.
29
+ def detect_nulls(value = nil)
30
+ @detect_nulls = value if !value.nil?
31
+ @detect_nulls
25
32
  end
26
33
 
34
+ # Adds a field to the graph type that is resolved to an attribute on the model.
35
+ # @param attribute Symbol with the name of the attribute on the model
36
+ # @param description Description for the field
37
+ # @param name Name of the field to use. By default, the attribute name is camelized.
38
+ # @param nullable Set to false to force the field to be non-null. By default, nullability is automatically detected.
39
+ # @param deprecation_reason Sets the deprecation reason on the field.
40
+ def attr(attribute, name: attribute.to_s.camelize(:lower), nullable: nil, description: nil, deprecation_reason: nil, &block)
41
+ name = name.to_sym unless name.is_a?(Symbol)
42
+
43
+ options = {
44
+ name: name,
45
+ nullable: nullable,
46
+ description: description,
47
+ deprecation_reason: deprecation_reason
48
+ }
49
+
50
+ DefinitionHelpers.define_attribute(@graph_type, @base_model_type, @model_type, @path, attribute, @object_to_model, options, nullable, &block)
51
+ end
52
+
53
+ # Flattens an associated model into the graph type, allowing to you adds its attributes as if they existed on the parent model.
54
+ # @param association Name of the association to use. Polymorphic belongs_to associations are not supported.
27
55
  def proxy_to(association, &block)
28
- DefinitionHelpers.define_proxy(graph_type, model_type, model_type, [], association, object_to_model, &block)
56
+ DefinitionHelpers.define_proxy(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, @detect_nulls, &block)
29
57
  end
30
58
 
31
- def has_one(association, **options)
32
- DefinitionHelpers.define_has_one(graph_type, model_type, model_type, [], association, object_to_model, options)
59
+ def has_one(association, name: association.to_s.camelize(:lower), nullable: nil, description: nil, deprecation_reason: nil)
60
+ name = name.to_sym unless name.is_a?(Symbol)
61
+
62
+ options = {
63
+ name: name,
64
+ nullable: nullable,
65
+ description: description,
66
+ deprecation_reason: deprecation_reason
67
+ }
68
+
69
+ DefinitionHelpers.define_has_one(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options, @detect_nulls)
33
70
  end
34
71
 
35
- def has_many_connection(association, **options)
36
- DefinitionHelpers.define_has_many_connection(graph_type, model_type, model_type, [], association, object_to_model, options)
72
+ def has_many_connection(association, name: association.to_s.camelize(:lower), nullable: nil, description: nil, deprecation_reason: nil, **goco_options)
73
+ name = name.to_sym unless name.is_a?(Symbol)
74
+
75
+ options = goco_options.merge({
76
+ name: name,
77
+ nullable: nullable,
78
+ description: description,
79
+ deprecation_reason: deprecation_reason
80
+ })
81
+
82
+ DefinitionHelpers.define_has_many_connection(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options, @detect_nulls)
37
83
  end
38
84
 
39
- def has_many_array(association, **options)
40
- DefinitionHelpers.define_has_many_array(graph_type, model_type, model_type, [], association, object_to_model, options)
85
+ def has_many_array(association, name: association.to_s.camelize(:lower), nullable: nil, description: nil, deprecation_reason: nil, type: nil)
86
+ name = name.to_sym unless name.is_a?(Symbol)
87
+
88
+ options = {
89
+ name: name,
90
+ type: type,
91
+ nullable: nullable,
92
+ description: description,
93
+ deprecation_reason: deprecation_reason
94
+ }
95
+
96
+ DefinitionHelpers.define_has_many_array(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options, @detect_nulls)
41
97
  end
42
98
 
43
99
  def field(*args, &block)
44
- defined_field = GraphQL::Define::AssignObjectField.call(graph_type, *args, &block)
100
+ defined_field = GraphQL::Define::AssignObjectField.call(@graph_type, *args, &block)
101
+ name = defined_field.name
102
+ name = name.to_sym unless name.is_a?(Symbol)
45
103
 
46
- DefinitionHelpers.register_field_metadata(graph_type, defined_field.name, {
104
+ DefinitionHelpers.register_field_metadata(@graph_type, name, {
47
105
  macro: :field,
48
106
  macro_type: :custom,
49
- path: [],
50
- base_model_type: @model_type.to_s.classify.constantize,
51
- model_type: @model_type.to_s.classify.constantize,
52
- object_to_base_model: object_to_model
107
+ path: @path,
108
+ base_model_type: @base_model_type,
109
+ model_type: @model_type,
110
+ object_to_base_model: @object_to_model
53
111
  })
54
112
 
55
113
  defined_field
@@ -0,0 +1,39 @@
1
+ module GraphQL
2
+ module Models
3
+ module DatabaseTypes
4
+ TypeStruct = Struct.new(:input, :output)
5
+
6
+ def self.registered_type(database_type)
7
+ @registered_types ||= {}.with_indifferent_access
8
+
9
+ result = @registered_types[database_type]
10
+ return nil if result.nil?
11
+
12
+ if !result.input.is_a?(GraphQL::BaseType) || !result.output.is_a?(GraphQL::BaseType)
13
+ input = result.input
14
+ output = result.output
15
+
16
+ input = input.call if input.is_a?(Proc)
17
+ output = output.call if output.is_a?(Proc)
18
+
19
+ input = input.constantize if input.is_a?(String)
20
+ output = output.constantize if output.is_a?(String)
21
+
22
+ TypeStruct.new(input, output)
23
+ else
24
+ result
25
+ end
26
+ end
27
+
28
+ def self.register(database_type, output_type, input_type = output_type)
29
+ @registered_types ||= {}.with_indifferent_access
30
+ @registered_types[database_type] = TypeStruct.new(input_type, output_type)
31
+ end
32
+ end
33
+
34
+ DatabaseTypes.register(:boolean, GraphQL::BOOLEAN_TYPE)
35
+ DatabaseTypes.register(:integer, GraphQL::INT_TYPE)
36
+ DatabaseTypes.register(:float, GraphQL::FLOAT_TYPE)
37
+ DatabaseTypes.register(:string, GraphQL::STRING_TYPE)
38
+ end
39
+ end
@@ -1,7 +1,7 @@
1
1
  module GraphQL
2
2
  module Models
3
3
  module DefinitionHelpers
4
- def self.define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, &block)
4
+ def self.define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, detect_nulls, &block)
5
5
  reflection = model_type.reflect_on_association(association)
6
6
  raise ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
7
7
  raise ArgumentError.new("Cannot proxy to polymorphic association #{association} on model #{model_type.name}") if reflection.polymorphic?
@@ -9,7 +9,15 @@ module GraphQL
9
9
 
10
10
  return unless block_given?
11
11
 
12
- proxy = ProxyBlock.new(graph_type, base_model_type, reflection.klass, [*path, association], object_to_model)
12
+ proxy = BackedByModel.new(
13
+ graph_type,
14
+ reflection.klass,
15
+ base_model_type: base_model_type,
16
+ path: [*path, association],
17
+ object_to_model: object_to_model,
18
+ detect_nulls: detect_nulls && Reflection.is_required(model_type, association)
19
+ )
20
+
13
21
  proxy.instance_exec(&block)
14
22
  end
15
23
 
@@ -18,36 +26,32 @@ module GraphQL
18
26
  ## Ordinary has_one/belongs_to associations
19
27
  ############################################
20
28
 
21
- return -> { "#{reflection.klass.name}Graph".constantize } if !reflection.polymorphic?
22
-
23
- ############################################
24
- ## Polymorphic associations
25
- ############################################
26
-
27
- # For polymorphic associations, we look for a validator that limits the types of entities that could be
28
- # used, and use it to build a union. If we can't find one, raise an error.
29
+ if reflection.polymorphic?
30
+ # For polymorphic associations, we look for a validator that limits the types of entities that could be
31
+ # used, and use it to build a union. If we can't find one, raise an error.
29
32
 
30
- model_type = reflection.active_record
31
- valid_types = detect_inclusion_values(model_type, reflection.foreign_type)
33
+ model_type = reflection.active_record
34
+ valid_types = Reflection.possible_values(model_type, reflection.foreign_type)
32
35
 
33
- if valid_types.blank?
34
- fail ArgumentError.new("Cannot include polymorphic #{reflection.name} association on model #{model_type.name}, because it does not define an inclusion validator on #{reflection.foreign_type}")
35
- end
36
+ if valid_types.blank?
37
+ fail ArgumentError.new("Cannot include polymorphic #{reflection.name} association on model #{model_type.name}, because it does not define an inclusion validator on #{reflection.foreign_type}")
38
+ end
36
39
 
37
- return ->() do
38
- graph_types = valid_types.map { |t| "#{t}Graph".safe_constantize }.compact
40
+ graph_types = valid_types.map { |t| GraphQL::Models.get_graphql_type(t) }.compact
39
41
 
40
42
  GraphQL::UnionType.define do
41
43
  name "#{model_type.name}#{reflection.foreign_type.classify}"
42
44
  description "Objects that can be used as #{reflection.foreign_type.titleize.downcase} on #{model_type.name.titleize.downcase}"
43
45
  possible_types graph_types
44
46
  end
47
+ else
48
+ GraphQL::Models.get_graphql_type!(reflection.klass)
45
49
  end
46
50
  end
47
51
 
48
52
  # Adds a field to the graph type which is resolved by accessing a has_one association on the model. Traverses
49
53
  # across has_one associations specified in the path. The resolver returns a promise.
50
- def self.define_has_one(graph_type, base_model_type, model_type, path, association, object_to_model, options)
54
+ def self.define_has_one(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
51
55
  reflection = model_type.reflect_on_association(association)
52
56
 
53
57
  fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
@@ -55,10 +59,9 @@ module GraphQL
55
59
 
56
60
  # Define the field for the association itself
57
61
 
58
- camel_name = options[:name] || association.to_s.camelize(:lower)
59
- camel_name = camel_name.to_sym if camel_name.is_a?(String)
60
-
61
- type_lambda = resolve_has_one_type(reflection)
62
+ camel_name = options[:name]
63
+ association_graphql_type = resolve_has_one_type(reflection)
64
+ association_graphql_type = resolve_nullability(association_graphql_type, model_type, association, detect_nulls, options)
62
65
 
63
66
  DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
64
67
  macro: :has_one,
@@ -72,7 +75,7 @@ module GraphQL
72
75
 
73
76
  graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
74
77
  name camel_name.to_s
75
- type type_lambda
78
+ type association_graphql_type
76
79
  description options[:description] if options.include?(:description)
77
80
  deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
78
81
 
@@ -84,6 +87,7 @@ module GraphQL
84
87
 
85
88
  # Define the field for the associated model's ID
86
89
  id_field_name = :"#{camel_name}Id"
90
+ id_field_type = resolve_nullability(GraphQL::ID_TYPE, model_type, association, detect_nulls, options)
87
91
 
88
92
  DefinitionHelpers.register_field_metadata(graph_type, id_field_name, {
89
93
  macro: :has_one,
@@ -103,7 +107,7 @@ module GraphQL
103
107
 
104
108
  graph_type.fields[id_field_name.to_s] = GraphQL::Field.define do
105
109
  name id_field_name.to_s
106
- type types.ID
110
+ type id_field_type
107
111
  deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
108
112
 
109
113
  resolve -> (model, args, context) do
@@ -123,14 +127,29 @@ module GraphQL
123
127
  end
124
128
  end
125
129
 
126
- def self.define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options)
130
+ def self.define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
127
131
  reflection = model_type.reflect_on_association(association)
128
132
 
129
133
  fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
130
134
  fail ArgumentError.new("Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_array") unless [:has_many].include?(reflection.macro)
131
135
 
132
- type_lambda = options[:type] || -> { types[!"#{reflection.klass.name}Graph".constantize] }
133
- camel_name = options[:name] || association.to_s.camelize(:lower).to_sym
136
+ association_type = options[:type] || GraphQL::Models.get_graphql_type!(reflection.klass)
137
+
138
+ if !association_type.is_a?(GraphQL::ListType)
139
+ association_type = association_type.to_non_null_type.to_list_type
140
+ end
141
+
142
+ id_field_type = GraphQL::ID_TYPE.to_non_null_type.to_list_type
143
+
144
+ # The has_many associations are a little special. Instead of checking for a presence validator, we instead assume
145
+ # that the outer type should be non-null, unless detect_nulls is false. In other words, we prefer an empty
146
+ # array for the association, rather than null.
147
+ if (options[:nullable] == nil && detect_nulls) || options[:nullable] == false
148
+ association_type = association_type.to_non_null_type
149
+ id_field_type = id_field_type.to_non_null_type
150
+ end
151
+
152
+ camel_name = options[:name]
134
153
 
135
154
  DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
136
155
  macro: :has_many_array,
@@ -144,7 +163,7 @@ module GraphQL
144
163
 
145
164
  graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
146
165
  name camel_name.to_s
147
- type type_lambda
166
+ type association_type
148
167
  description options[:description] if options.include?(:description)
149
168
  deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
150
169
 
@@ -171,7 +190,7 @@ module GraphQL
171
190
 
172
191
  graph_type.fields[id_field_name.to_s] = GraphQL::Field.define do
173
192
  name id_field_name.to_s
174
- type types[!types.ID]
193
+ type id_field_type
175
194
  deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
176
195
 
177
196
  resolve -> (model, args, context) do
@@ -183,14 +202,19 @@ module GraphQL
183
202
  end
184
203
  end
185
204
 
186
- def self.define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options)
205
+ def self.define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options, detect_nulls)
187
206
  reflection = model_type.reflect_on_association(association)
188
207
 
189
208
  fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
190
209
  fail ArgumentError.new("Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_connection") unless [:has_many].include?(reflection.macro)
191
210
 
192
- type_lambda = -> { "#{reflection.klass.name}Graph".constantize.connection_type }
193
- camel_name = options[:name] || association.to_s.camelize(:lower).to_sym
211
+ connection_type = GraphQL::Models.get_graphql_type!(reflection.klass).connection_type
212
+
213
+ if (options[:nullable] == nil && detect_nulls) || options[:nullable] == false
214
+ connection_type = connection_type.to_non_null_type
215
+ end
216
+
217
+ camel_name = options[:name]
194
218
 
195
219
  DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
196
220
  macro: :has_many_connection,
@@ -202,12 +226,20 @@ module GraphQL
202
226
  object_to_base_model: object_to_model
203
227
  })
204
228
 
205
- GraphQL::Define::AssignConnection.call(graph_type, camel_name, type_lambda) do
206
- resolve -> (model, args, context) do
207
- return nil unless model
208
-
209
- # TODO: Figure out a way to remove this from the gem. It's only applicable to GoCo's codebase.
210
- GraphSupport.secure(model.public_send(association), context, permission: options[:permission] || :read)
229
+ # TODO: Figure out a way to remove this from the gem. It's only applicable to GoCo's codebase.
230
+ if Object.const_defined?('GraphSupport') && GraphSupport.respond_to?(:secure)
231
+ GraphQL::Define::AssignConnection.call(graph_type, camel_name, connection_type) do
232
+ resolve -> (model, args, context) do
233
+ return nil unless model
234
+ GraphSupport.secure(model.public_send(association), context, permission: options[:permission] || :read)
235
+ end
236
+ end
237
+ else
238
+ GraphQL::Define::AssignConnection.call(graph_type, camel_name, connection_type) do
239
+ resolve -> (model, args, context) do
240
+ return nil unless model
241
+ model.public_send(association)
242
+ end
211
243
  end
212
244
  end
213
245
  end
@@ -1,104 +1,50 @@
1
1
  module GraphQL
2
2
  module Models
3
3
  module DefinitionHelpers
4
- def self.type_to_graphql_type(type)
5
- registered_type = ScalarTypes.registered_type(type)
6
- if registered_type
7
- return registered_type.is_a?(Proc) ? registered_type.call : registered_type
4
+ def self.resolve_nullability(graphql_type, model_class, attribute_or_association, detect_nulls, options)
5
+ # If detect_nulls is true, it means that everything on the path (ie, between base_model_class and model_class) is non null.
6
+ # So for example, if we're five levels deep inside of proxy_to blocks, but every single association along the way has
7
+ # a presence validator, then `detect_nulls` is false. Thus, we can take it one step further and enforce nullability on the
8
+ # attribute itself.
9
+ nullable = options[:nullable]
10
+
11
+ if nullable == nil
12
+ nullable = !(detect_nulls && Reflection.is_required(model_class, attribute_or_association))
8
13
  end
9
14
 
10
- case type
11
- when :boolean
12
- types.Boolean
13
- when :integer
14
- types.Int
15
- when :float
16
- types.Float
17
- when :daterange
18
- inner_type = type_to_graphql_type(:date)
19
- types[!inner_type]
20
- when :tsrange
21
- inner_type = type_to_graphql_type(:datetime)
22
- types[!inner_type]
15
+ if nullable == false
16
+ graphql_type = graphql_type.to_non_null_type
23
17
  else
24
- types.String
18
+ graphql_type
25
19
  end
26
20
  end
27
21
 
28
- def self.get_column(model_type, name)
29
- col = model_type.columns.detect { |c| c.name == name.to_s }
30
- return nil unless col
22
+ def self.define_attribute(graph_type, base_model_class, model_class, path, attribute, object_to_model, options, detect_nulls, &block)
23
+ attribute_graphql_type = Reflection.attribute_graphql_type(model_class, attribute).output
24
+ attribute_graphql_type = resolve_nullability(attribute_graphql_type, model_class, attribute, detect_nulls, options)
31
25
 
32
- if model_type.graphql_enum_types.include?(name)
33
- graphql_type = model_type.graphql_enum_types[name]
34
- else
35
- graphql_type = type_to_graphql_type(col.type)
36
- end
37
-
38
- if col.array
39
- graphql_type = types[graphql_type]
40
- end
41
-
42
- return OpenStruct.new({
43
- is_range: /range\z/ === col.type.to_s,
44
- camel_name: name.to_s.camelize(:lower).to_sym,
45
- graphql_type: graphql_type,
46
- nullable: col.null
47
- })
48
- end
49
-
50
- def self.get_column!(model_type, name)
51
- col = get_column(model_type, name)
52
- raise ArgumentError.new("The attribute #{name} wasn't found on model #{model_type.name}.") unless col
53
- col
54
- end
55
-
56
- def self.range_to_graphql(value)
57
- return nil unless value
58
-
59
- begin
60
- [value.first, value.last_included]
61
- rescue TypeError
62
- [value.first, value.last]
63
- end
64
- end
65
-
66
- # Adds a field to the graph type which is resolved by accessing an attribute on the model. Traverses
67
- # across has_one associations specified in the path. The resolver returns a promise.
68
- # @param graph_type The GraphQL::ObjectType that the field is being added to
69
- # @param model_type The class object for the model that defines the attribute
70
- # @param path The associations (in order) that need to be loaded, starting from the graph_type's model
71
- # @param attribute The name of the attribute that is accessed on the target model_type
72
- def self.define_attribute(graph_type, base_model_type, model_type, path, attribute, object_to_model, options, &block)
73
- column = get_column!(model_type, attribute)
74
- field_name = options[:name] || column.camel_name
26
+ field_name = options[:name]
75
27
 
76
28
  DefinitionHelpers.register_field_metadata(graph_type, field_name, {
77
29
  macro: :attr,
78
30
  macro_type: :attribute,
79
31
  path: path,
80
32
  attribute: attribute,
81
- base_model_type: base_model_type,
82
- model_type: model_type,
33
+ base_model_class: base_model_class,
34
+ model_class: model_class,
83
35
  object_to_base_model: object_to_model
84
36
  })
85
37
 
86
38
  graph_type.fields[field_name.to_s] = GraphQL::Field.define do
87
39
  name field_name.to_s
88
- type column.graphql_type
40
+ type attribute_graphql_type
89
41
  description options[:description] if options.include?(:description)
90
42
  deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
91
43
 
92
44
  resolve -> (model, args, context) do
93
- return nil unless model
94
-
95
- if column.is_range
96
- DefinitionHelpers.range_to_graphql(model.public_send(attribute))
97
- else
98
- model.public_send(attribute)
99
- end
45
+ model&.public_send(attribute)
100
46
  end
101
-
47
+
102
48
  instance_exec(&block) if block
103
49
  end
104
50
  end
@@ -117,32 +117,6 @@ module GraphQL
117
117
  return model
118
118
  end
119
119
 
120
- # Detects the values that are valid for an attribute by looking at the inclusion validators
121
- def self.detect_inclusion_values(model_type, attribute)
122
- # Get all of the inclusion validators
123
- validators = model_type.validators_on(attribute).select { |v| v.is_a?(ActiveModel::Validations::InclusionValidator) }
124
-
125
- # Ignore any inclusion validators that are using the 'if' or 'unless' options
126
- validators = validators.reject { |v| v.options.include?(:if) || v.options.include?(:unless) || v.options[:in].blank? }
127
- return nil unless validators.any?
128
- return validators.map { |v| v.options[:in] }.reduce(:&)
129
- end
130
-
131
- def self.detect_is_required(model_type, attr_or_assoc)
132
- col = model_type.columns.detect { |c| c.name == attr_or_assoc.to_s }
133
- return true if col && !col.null
134
-
135
- validators = model_type.validators_on(attr_or_assoc)
136
- .select { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
137
- .reject { |v| v.options.include?(:if) || v.options.include?(:unless) }
138
-
139
- return true if validators.any?
140
-
141
- # The column is nullable, and there are no unconditional presence validators,
142
- # so it's at least sometimes optional
143
- false
144
- end
145
-
146
120
  # Stores metadata about GraphQL fields that are available on this model's GraphQL type.
147
121
  # @param metadata Should be a hash that contains information about the field's definition, including :macro and :type
148
122
  def self.register_field_metadata(graph_type, field_name, metadata)
@@ -23,31 +23,23 @@ module GraphQL::Models
23
23
  GraphQL::Define::TypeDefiner.instance
24
24
  end
25
25
 
26
- def attr(attribute, type: nil, name: nil, required: false)
26
+ def attr(attribute, type: nil, name: nil, required: nil)
27
27
  attribute = attribute.to_sym if attribute.is_a?(String)
28
28
 
29
29
  if type.nil? && !model_type
30
30
  fail ArgumentError.new("You must specify a type for attribute #{name}, because its model type is not known until runtime.")
31
31
  end
32
32
 
33
- if model_type
34
- column = DefinitionHelpers.get_column(model_type, attribute)
35
-
36
- if column.nil? && type.nil?
37
- fail ArgumentError.new("You must specify a type for attribute #{name}, because it's not a column on #{model_type}.")
38
- end
33
+ if type.nil? && (attribute == :id || foreign_keys.include?(attribute))
34
+ type = types.ID
35
+ end
39
36
 
40
- if column
41
- type ||= begin
42
- if attribute == :id || foreign_keys.include?(attribute)
43
- type = types.ID
44
- else
45
- type = column.graphql_type
46
- end
47
- end
37
+ if type.nil? && model_type
38
+ type = Reflection.attribute_graphql_type(model_type, attribute).input
39
+ end
48
40
 
49
- required = DefinitionHelpers.detect_is_required(model_type, attribute)
50
- end
41
+ if required.nil?
42
+ required = model_type ? Reflection.is_required(model_type, attribute) : false
51
43
  end
52
44
 
53
45
  name ||= attribute.to_s.camelize(:lower)
@@ -118,7 +110,7 @@ module GraphQL::Models
118
110
  end
119
111
 
120
112
  has_many = reflection.macro == :has_many
121
- required = DefinitionHelpers.detect_is_required(model_type, association)
113
+ required = Reflection.is_required(model_type, association)
122
114
 
123
115
  map = MutationFieldMap.new(reflection.klass, find_by: find_by, null_behavior: null_behavior)
124
116
  map.name = name || association.to_s.camelize(:lower)
@@ -0,0 +1,57 @@
1
+ # Exposes utility methods for getting metadata out of active record models
2
+ module GraphQL::Models
3
+ module Reflection
4
+ class << self
5
+ # Returns the possible values for an attribute on a model by examining inclusion validators
6
+ def possible_values(model_class, attribute)
7
+ # Get all of the inclusion validators
8
+ validators = model_class.validators_on(attribute).select { |v| v.is_a?(ActiveModel::Validations::InclusionValidator) }
9
+
10
+ # Ignore any inclusion validators that are using the 'if' or 'unless' options
11
+ validators = validators.reject { |v| v.options.include?(:if) || v.options.include?(:unless) || v.options[:in].blank? }
12
+ return nil unless validators.any?
13
+ return validators.map { |v| v.options[:in] }.reduce(:&)
14
+ end
15
+
16
+ # Determines if the attribute (or association) is required by examining presence validators
17
+ # and the nullability of the column in the database
18
+ def is_required(model_class, attr_or_assoc)
19
+ return true if model_class.columns_hash[attr_or_assoc.to_s]&.null == false
20
+
21
+ model_class.validators_on(attr_or_assoc)
22
+ .select { |v| v.is_a?(ActiveModel::Validations::PresenceValidator) }
23
+ .reject { |v| v.options.include?(:if) || v.options.include?(:unless) }
24
+ .any?
25
+ end
26
+
27
+ # Returns a struct that tells you the input and output GraphQL types for an attribute
28
+ def attribute_graphql_type(model_class, attribute)
29
+ # See if it's an enum
30
+ if model_class.graphql_enum_types.include?(attribute)
31
+ type = model_class.graphql_enum_types[attribute]
32
+ DatabaseTypes::TypeStruct.new(type, type)
33
+ else
34
+ # See if it's a registered scalar type
35
+ active_record_type = model_class.type_for_attribute(attribute.to_s)
36
+
37
+ if active_record_type.type.nil?
38
+ fail ArgumentError, "The type for attribute #{attribute} wasn't found on #{model_class.name}"
39
+ end
40
+
41
+ result = DatabaseTypes.registered_type(active_record_type.type)
42
+
43
+ if !result
44
+ fail RuntimeError, "The type #{active_record_type} is not registered with DatabaseTypes (attribute #{attribute} on #{model_class.name})"
45
+ end
46
+
47
+ # Arrays: Rails doesn't have a generalized way to detect arrays, so we use this method to do it:
48
+ if active_record_type.class.name.ends_with?('Array')
49
+ DatabaseTypes::TypeStruct.new(result.input.to_list_type, result.output.to_list_type)
50
+ else
51
+ result
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,5 +1,5 @@
1
1
  module GraphQL
2
2
  module Models
3
- VERSION = "0.9.1"
3
+ VERSION = "0.10.0-alpha1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-activerecord
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 0.10.0.pre.alpha1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Foster
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-01-09 00:00:00.000000000 Z
11
+ date: 2017-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -176,6 +176,7 @@ files:
176
176
  - lib/graphql/models/association_load_request.rb
177
177
  - lib/graphql/models/attribute_loader.rb
178
178
  - lib/graphql/models/backed_by_model.rb
179
+ - lib/graphql/models/database_types.rb
179
180
  - lib/graphql/models/definer.rb
180
181
  - lib/graphql/models/definition_helpers.rb
181
182
  - lib/graphql/models/definition_helpers/associations.rb
@@ -191,12 +192,10 @@ files:
191
192
  - lib/graphql/models/mutation_helpers/validation.rb
192
193
  - lib/graphql/models/mutation_helpers/validation_error.rb
193
194
  - lib/graphql/models/mutator.rb
194
- - lib/graphql/models/object_type.rb
195
195
  - lib/graphql/models/promise_relation_connection.rb
196
- - lib/graphql/models/proxy_block.rb
196
+ - lib/graphql/models/reflection.rb
197
197
  - lib/graphql/models/relation_load_request.rb
198
198
  - lib/graphql/models/relation_loader.rb
199
- - lib/graphql/models/scalar_types.rb
200
199
  - lib/graphql/models/version.rb
201
200
  homepage: http://github.com/goco-inc/graphql-activerecord
202
201
  licenses:
@@ -213,9 +212,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
213
212
  version: '0'
214
213
  required_rubygems_version: !ruby/object:Gem::Requirement
215
214
  requirements:
216
- - - ">="
215
+ - - ">"
217
216
  - !ruby/object:Gem::Version
218
- version: '0'
217
+ version: 1.3.1
219
218
  requirements: []
220
219
  rubyforge_project:
221
220
  rubygems_version: 2.4.8
@@ -1,120 +0,0 @@
1
- module GraphQL
2
- module Models
3
- module ObjectType
4
- class << self
5
- DEFAULT_OBJECT_TO_MODEL = -> (object) { object }
6
-
7
- def object_to_model(graph_type, model_proc)
8
- graph_type.instance_variable_set(:@unscoped_object_to_model, model_proc)
9
- end
10
-
11
- def model_type(graph_type, model_type)
12
- model_type = model_type.to_s.classify.constantize unless model_type.is_a?(Class)
13
-
14
- object_to_model = -> (object) do
15
- model_proc = graph_type.instance_variable_get(:@unscoped_object_to_model)
16
- if model_proc
17
- model_proc.call(object)
18
- else
19
- DEFAULT_OBJECT_TO_MODEL.call(object)
20
- end
21
- end
22
-
23
- graph_type.instance_variable_set(:@unscoped_model_type, model_type)
24
-
25
- graph_type.fields['id'] = GraphQL::Field.define do
26
- name 'id'
27
- type !types.ID
28
- resolve -> (object, args, context) { object.gid }
29
- end
30
-
31
- if GraphQL::Models.node_interface_proc
32
- node_interface = GraphQL::Models.node_interface_proc.call
33
- graph_type.interfaces = [*graph_type.interfaces, node_interface].uniq
34
- end
35
-
36
- graph_type.fields['rid'] = GraphQL::Field.define do
37
- name 'rid'
38
- type !types.String
39
- resolve -> (object, args, context) do
40
- model = object_to_model.call(object)
41
- model.id
42
- end
43
- end
44
-
45
- graph_type.fields['rtype'] = GraphQL::Field.define do
46
- name 'rtype'
47
- type !types.String
48
- resolve -> (object, args, context) do
49
- model = object_to_model.call(object)
50
- model.class.name
51
- end
52
- end
53
-
54
- if model_type.columns.detect { |c| c.name == 'created_at'}
55
- DefinitionHelpers.define_attribute(graph_type, model_type, model_type, [], :created_at, object_to_model, {})
56
- end
57
-
58
- if model_type.columns.detect { |c| c.name == 'updated_at'}
59
- DefinitionHelpers.define_attribute(graph_type, model_type, model_type, [], :updated_at, object_to_model, {})
60
- end
61
- end
62
-
63
- def proxy_to(graph_type, association, &block)
64
- ensure_has_model_type(graph_type, __method__)
65
- object_to_model = graph_type.instance_variable_get(:@unscoped_object_to_model) || DEFAULT_OBJECT_TO_MODEL
66
- DefinitionHelpers.define_proxy(graph_type, resolve_model_type(graph_type), resolve_model_type(graph_type), [], association, object_to_model, &block)
67
- end
68
-
69
- def attr(graph_type, name, **options, &block)
70
- ensure_has_model_type(graph_type, __method__)
71
- object_to_model = graph_type.instance_variable_get(:@unscoped_object_to_model) || DEFAULT_OBJECT_TO_MODEL
72
- DefinitionHelpers.define_attribute(graph_type, resolve_model_type(graph_type), resolve_model_type(graph_type), [], name, object_to_model, options, &block)
73
- end
74
-
75
- def has_one(graph_type, association, **options)
76
- ensure_has_model_type(graph_type, __method__)
77
- object_to_model = graph_type.instance_variable_get(:@unscoped_object_to_model) || DEFAULT_OBJECT_TO_MODEL
78
- DefinitionHelpers.define_has_one(graph_type, resolve_model_type(graph_type), resolve_model_type(graph_type), [], association, object_to_model, options)
79
- end
80
-
81
- def has_many_connection(graph_type, association, **options)
82
- ensure_has_model_type(graph_type, __method__)
83
- object_to_model = graph_type.instance_variable_get(:@unscoped_object_to_model) || DEFAULT_OBJECT_TO_MODEL
84
- DefinitionHelpers.define_has_many_connection(graph_type, resolve_model_type(graph_type), resolve_model_type(graph_type), [], association, object_to_model, options)
85
- end
86
-
87
- def has_many_array(graph_type, association, **options)
88
- ensure_has_model_type(graph_type, __method__)
89
- object_to_model = graph_type.instance_variable_get(:@unscoped_object_to_model) || DEFAULT_OBJECT_TO_MODEL
90
- DefinitionHelpers.define_has_many_array(graph_type, resolve_model_type(graph_type), resolve_model_type(graph_type), [], association, object_to_model, options)
91
- end
92
-
93
- def backed_by_model(graph_type, model_type, &block)
94
- model_type = model_type.to_s.classify.constantize unless model_type.is_a?(Class)
95
-
96
- backer = GraphQL::Models::BackedByModel.new(graph_type, model_type)
97
- backer.instance_exec(&block)
98
- end
99
-
100
- private
101
-
102
- def resolve_model_type(graph_type)
103
- graph_type.instance_variable_get(:@unscoped_model_type)
104
- end
105
-
106
- def ensure_has_model_type(graph_type, method)
107
- fail RuntimeError.new("You must call model_type before using the #{method} method.") unless graph_type.instance_variable_get(:@unscoped_model_type)
108
- end
109
- end
110
-
111
- # Attach the methods to ObjectType
112
- extensions = ObjectType.methods(false).reduce({}) do |memo, method|
113
- memo[method] = ObjectType.method(method)
114
- memo
115
- end
116
-
117
- GraphQL::ObjectType.accepts_definitions(extensions)
118
- end
119
- end
120
- end
@@ -1,52 +0,0 @@
1
- module GraphQL
2
- module Models
3
- class ProxyBlock
4
- def initialize(graph_type, base_model_type, model_type, path, object_to_model)
5
- @path = path
6
- @base_model_type = base_model_type
7
- @model_type = model_type
8
- @graph_type = graph_type
9
- @object_to_model = object_to_model
10
- end
11
-
12
- def types
13
- GraphQL::Define::TypeDefiner.instance
14
- end
15
-
16
- def attr(name, **options, &block)
17
- DefinitionHelpers.define_attribute(@graph_type, @base_model_type, @model_type, @path, name, @object_to_model, options, &block)
18
- end
19
-
20
- def proxy_to(association, &block)
21
- DefinitionHelpers.define_proxy(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, &block)
22
- end
23
-
24
- def has_one(association, **options)
25
- DefinitionHelpers.define_has_one(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options)
26
- end
27
-
28
- def has_many_connection(association, **options)
29
- DefinitionHelpers.define_has_many_connection(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options)
30
- end
31
-
32
- def has_many_array(association, **options)
33
- DefinitionHelpers.define_has_many_array(@graph_type, @base_model_type, @model_type, @path, association, @object_to_model, options)
34
- end
35
-
36
- def field(*args, &block)
37
- defined_field = GraphQL::Define::AssignObjectField.call(@graph_type, *args, &block)
38
-
39
- DefinitionHelpers.register_field_metadata(@graph_type, defined_field.name, {
40
- macro: :field,
41
- macro_type: :custom,
42
- path: @path,
43
- base_model_type: @base_model_type,
44
- model_type: @model_type,
45
- object_to_base_model: @object_to_model
46
- })
47
-
48
- defined_field
49
- end
50
- end
51
- end
52
- end
@@ -1,15 +0,0 @@
1
- module GraphQL
2
- module Models
3
- module ScalarTypes
4
- def self.registered_type(database_type)
5
- @registered_types ||= {}.with_indifferent_access
6
- @registered_types[database_type]
7
- end
8
-
9
- def self.register(database_type, graphql_type)
10
- @registered_types ||= {}.with_indifferent_access
11
- @registered_types[database_type] = graphql_type
12
- end
13
- end
14
- end
15
- end