graphql-activerecord 0.9.1 → 0.10.0.pre.alpha1

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