graphql-activerecord 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ef1f87b7695a19936a0776a5a9d1fe43364bbfe7
4
+ data.tar.gz: c3aece35fd294936549655ea4d7a6282356803d9
5
+ SHA512:
6
+ metadata.gz: 54edee615092c39ef2000be4519345706da498d4742d050b074a399b7ff8512b8e83846d2bb88ed51c3763e805b41ad32cf21a1b545b11a0fb42483d093661b8
7
+ data.tar.gz: 8516a38b326591d06b28f94748f411b6c7c2d471efb4e9495fb88fd510e8b8523eaef3752a5bfc5d084850e5c15ce657c7dcb5e9ada387657c63428510d6e600
@@ -0,0 +1,54 @@
1
+ require 'graphql'
2
+
3
+ require 'graphql/models/monkey_patches/graphql_query_context'
4
+ require 'graphql/models/monkey_patches/graphql_schema_middleware_chain'
5
+ require 'graphql/models/active_record_extension'
6
+ require 'graphql/models/middleware'
7
+
8
+ # Helpers
9
+ require 'graphql/models/definer'
10
+ require 'graphql/models/association_load_request'
11
+ require 'graphql/models/loader'
12
+
13
+ # Order matters...
14
+ require 'graphql/models/promise_relation_connection'
15
+ require 'graphql/models/relation_load_request'
16
+ require 'graphql/models/scalar_types'
17
+ require 'graphql/models/definition_helpers'
18
+ require 'graphql/models/definition_helpers/associations'
19
+ require 'graphql/models/definition_helpers/attributes'
20
+
21
+ require 'graphql/models/proxy_block'
22
+ require 'graphql/models/backed_by_model'
23
+ require 'graphql/models/object_type'
24
+
25
+
26
+ module GraphQL
27
+ module Models
28
+ class << self
29
+ attr_accessor :node_interface_proc
30
+ end
31
+
32
+ # Returns a promise that will traverse the associations and resolve to the model at the end of the path.
33
+ # You can use this to access associated models inside custom field resolvers, without losing optimization
34
+ # benefits.
35
+ def self.load_association(starting_model, path, context)
36
+ path = Array.wrap(path)
37
+ GraphQL::Models::DefinitionHelpers.load_and_traverse(starting_model, path, context)
38
+ end
39
+
40
+ def self.load_relation(relation)
41
+ request = RelationLoadRequest.new(relation)
42
+ request.load
43
+ end
44
+
45
+ def self.field_info(graph_type, field_name)
46
+ field_name = field_name.to_s
47
+
48
+ meta = graph_type.instance_variable_get(:@field_metadata)
49
+ return nil unless meta
50
+
51
+ meta[field_name]
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,63 @@
1
+ module GraphQL
2
+ module Models
3
+ module ActiveRecordExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ # Default options for graphql_enums
7
+ ENUM_OPTIONS = {
8
+ upcase: true
9
+ }
10
+
11
+ class_methods do
12
+ def graphql_enum_types
13
+ @_graphql_enum_types ||= {}.with_indifferent_access
14
+ end
15
+
16
+ # Defines a GraphQL enum type on the model
17
+ def graphql_enum(attribute, **options)
18
+ options = ENUM_OPTIONS.merge(options)
19
+ options[:name] ||= "#{self.name}#{attribute.to_s.classify}"
20
+ options[:description] ||= "#{attribute.to_s.titleize} field on #{self.name.titleize}"
21
+
22
+ if !options.include?(:values) && !options.include?(:type)
23
+ if defined_enums.include?(attribute.to_s)
24
+ options[:values] = defined_enums[attribute.to_s].keys.map { |ev| [options[:upcase] ? ev.upcase : ev, ev.titleize] }.to_h
25
+ else
26
+ fail ArgumentError.new("Could not auto-detect the values for enum #{attribute} on #{self.name}")
27
+ end
28
+ end
29
+
30
+ enum_type = graphql_enum_types[attribute]
31
+ unless enum_type
32
+ enum_type = options[:type] || GraphQL::EnumType.define do
33
+ name options[:name]
34
+ description options[:description]
35
+
36
+ options[:values].each do |value_name, desc|
37
+ value(value_name, desc)
38
+ end
39
+ end
40
+
41
+ graphql_enum_types[attribute] = enum_type
42
+ end
43
+
44
+ graphql_resolve(attribute) { send(attribute).try(:upcase) } if options[:upcase]
45
+ enum_type
46
+ end
47
+
48
+ def graphql_resolvers
49
+ @_graphql_resolvers ||= {}.with_indifferent_access
50
+ end
51
+
52
+ # Defines a custom method that is used to resolve this attribute's value when it is included
53
+ # on a GraphQL type.
54
+ def graphql_resolve(attribute, &block)
55
+ fail ArgumentError.new("#{__method__} requires a block") unless block_given?
56
+ graphql_resolvers[attribute] = block
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ ActiveRecord::Base.send(:include, GraphQL::Models::ActiveRecordExtension)
@@ -0,0 +1,97 @@
1
+ module GraphQL
2
+ module Models
3
+ class AssociationLoadRequest
4
+ attr_reader :base_model, :association, :context
5
+
6
+ def initialize(base_model, association_name, context)
7
+ @base_model = base_model
8
+ @association = base_model.association(association_name)
9
+ @context = context
10
+ end
11
+
12
+ ####################################################################
13
+ # Public members that all load requests should implement
14
+ ####################################################################
15
+
16
+ def load_type
17
+ case reflection.macro
18
+ when :belongs_to
19
+ :id
20
+ else
21
+ :relation
22
+ end
23
+ end
24
+
25
+ def load_target
26
+ case reflection.macro
27
+ when :belongs_to
28
+ base_model.send(reflection.foreign_key)
29
+ when :has_many
30
+ base_model.send(association.reflection.name)
31
+ else
32
+ # has_one, need to construct our own relation, because accessing the relation will load the model
33
+ condition = { reflection.foreign_key => base_model.id }
34
+
35
+ if reflection.options.include?(:as)
36
+ condition[reflection.type] = base_model.class.name
37
+ end
38
+
39
+ target_class.where(condition)
40
+ end
41
+ end
42
+
43
+ # If the value should be an array, make sure it's an array. If it should be a single value, make sure it's single.
44
+ # Passed in result could be a single model or an array of models.
45
+ def ensure_cardinality(result)
46
+ case reflection.macro
47
+ when :has_many
48
+ Array.wrap(result)
49
+ else
50
+ result.is_a?(Array) ? result[0] : result
51
+ end
52
+ end
53
+
54
+ # When the request is fulfilled, this method is called so that it can do whatever caching, etc. is needed
55
+ def fulfilled(result)
56
+ association.loaded!
57
+
58
+ if reflection.macro == :has_many
59
+ association.target.concat(result)
60
+ result.each do |m|
61
+ association.set_inverse_instance(m)
62
+ end
63
+ else
64
+ association.target = result
65
+ association.set_inverse_instance(result) if result
66
+ end
67
+ end
68
+
69
+ def load
70
+ loader.load(self)
71
+ end
72
+
73
+ #################################################################
74
+ # Public members specific to an association load request
75
+ #################################################################
76
+
77
+ def target_class
78
+ case when reflection.polymorphic?
79
+ base_model.send(reflection.foreign_type).constantize
80
+ else
81
+ reflection.klass
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def loader
88
+ @loader ||= Loader.for(target_class)
89
+ end
90
+
91
+ def reflection
92
+ association.reflection
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,59 @@
1
+ module GraphQL
2
+ module Models
3
+ class BackedByModel
4
+ attr_accessor :graph_type, :model_type, :object_to_model
5
+
6
+ DEFAULT_OBJECT_TO_MODEL = -> (obj) { obj }
7
+
8
+ def initialize(graph_type, model_type)
9
+ @graph_type = graph_type
10
+ @model_type = model_type
11
+ @object_to_model = DEFAULT_OBJECT_TO_MODEL
12
+ end
13
+
14
+ def types
15
+ GraphQL::Define::TypeDefiner.instance
16
+ end
17
+
18
+ def object_to_model(value = nil)
19
+ @object_to_model = value if value
20
+ @object_to_model
21
+ end
22
+
23
+ def attr(name, **options)
24
+ DefinitionHelpers.define_attribute(graph_type, model_type, model_type, [], name, object_to_model, options)
25
+ end
26
+
27
+ def proxy_to(association, &block)
28
+ DefinitionHelpers.define_proxy(graph_type, model_type, model_type, [], association, object_to_model, &block)
29
+ end
30
+
31
+ def has_one(association, **options)
32
+ DefinitionHelpers.define_has_one(graph_type, model_type, model_type, [], association, object_to_model, options)
33
+ end
34
+
35
+ def has_many_connection(association, **options)
36
+ DefinitionHelpers.define_has_many_connection(graph_type, model_type, model_type, [], association, object_to_model, options)
37
+ end
38
+
39
+ def has_many_array(association, **options)
40
+ DefinitionHelpers.define_has_many_array(graph_type, model_type, model_type, [], association, object_to_model, options)
41
+ end
42
+
43
+ def field(*args, &block)
44
+ defined_field = GraphQL::Define::AssignObjectField.call(graph_type, *args, &block)
45
+
46
+ DefinitionHelpers.register_field_metadata(graph_type, defined_field.name, {
47
+ macro: :field,
48
+ 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
53
+ })
54
+
55
+ defined_field
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # This is a helper class. It lets you build simple DSL's. Methods called against the class are
2
+ # converted into attributes in a hash.
3
+ module GraphQL
4
+ module Models
5
+ class Definer
6
+ def initialize(*methods)
7
+ @values = {}
8
+ methods.each do |m|
9
+ define_singleton_method(m) do |*args|
10
+ if args.blank?
11
+ @values[m] = nil
12
+ elsif args.length == 1
13
+ @values[m] = args[0]
14
+ else
15
+ @values[m] = args
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ def defined_values
22
+ @values
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,144 @@
1
+ module GraphQL
2
+ module Models
3
+ module DefinitionHelpers
4
+ def self.define_proxy(graph_type, base_model_type, model_type, path, association, object_to_model, &block)
5
+ reflection = model_type.reflect_on_association(association)
6
+ raise ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
7
+ raise ArgumentError.new("Cannot proxy to polymorphic association #{association} on model #{model_type.name}") if reflection.polymorphic?
8
+ raise ArgumentError.new("Cannot proxy to #{reflection.macro} association #{association} on model #{model_type.name}") unless [:has_one, :belongs_to].include?(reflection.macro)
9
+
10
+ return unless block_given?
11
+
12
+ proxy = ProxyBlock.new(graph_type, base_model_type, reflection.klass, [*path, association], object_to_model)
13
+ proxy.instance_exec(&block)
14
+ end
15
+
16
+ def self.resolve_has_one_type(reflection)
17
+ ############################################
18
+ ## Ordinary has_one/belongs_to associations
19
+ ############################################
20
+
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
+
30
+ model_type = reflection.active_record
31
+ valid_types = detect_inclusion_values(model_type, reflection.foreign_type)
32
+
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
+
37
+ return ->() do
38
+ graph_types = valid_types.map { |t| "#{t}Graph".safe_constantize }.compact
39
+
40
+ GraphQL::UnionType.define do
41
+ name "#{model_type.name}#{reflection.foreign_type.classify}"
42
+ description "Objects that can be used as #{reflection.foreign_type.titleize.downcase} on #{model_type.name.titleize.downcase}"
43
+ possible_types graph_types
44
+ end
45
+ end
46
+ end
47
+
48
+ # Adds a field to the graph type which is resolved by accessing a has_one association on the model. Traverses
49
+ # 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)
51
+ reflection = model_type.reflect_on_association(association)
52
+
53
+ fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
54
+ fail ArgumentError.new("Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_one") unless [:has_one, :belongs_to].include?(reflection.macro)
55
+
56
+ camel_name = options[:name] || association.to_s.camelize(:lower).to_sym
57
+ type_lambda = resolve_has_one_type(reflection)
58
+
59
+ DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
60
+ macro: :has_one,
61
+ macro_type: :association,
62
+ path: path,
63
+ association: association,
64
+ base_model_type: base_model_type,
65
+ model_type: model_type,
66
+ object_to_base_model: object_to_model
67
+ })
68
+
69
+ graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
70
+ name camel_name.to_s
71
+ type type_lambda
72
+ description options[:description] if options.include?(:description)
73
+ deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
74
+
75
+ resolve -> (model, args, context) do
76
+ return nil unless model
77
+ DefinitionHelpers.load_and_traverse(model, [association], context)
78
+ end
79
+ end
80
+ end
81
+
82
+ def self.define_has_many_array(graph_type, base_model_type, model_type, path, association, object_to_model, options)
83
+ reflection = model_type.reflect_on_association(association)
84
+
85
+ fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
86
+ fail ArgumentError.new("Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_array") unless [:has_many].include?(reflection.macro)
87
+
88
+ type_lambda = options[:type] || -> { types[!"#{reflection.klass.name}Graph".constantize] }
89
+ camel_name = options[:name] || association.to_s.camelize(:lower).to_sym
90
+
91
+ DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
92
+ macro: :has_many_array,
93
+ macro_type: :association,
94
+ path: path,
95
+ association: association,
96
+ base_model_type: base_model_type,
97
+ model_type: model_type,
98
+ object_to_base_model: object_to_model
99
+ })
100
+
101
+ graph_type.fields[camel_name.to_s] = GraphQL::Field.define do
102
+ name camel_name.to_s
103
+ type type_lambda
104
+ description options[:description] if options.include?(:description)
105
+ deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
106
+
107
+ resolve -> (model, args, context) do
108
+ return nil unless model
109
+ DefinitionHelpers.load_and_traverse(model, [association], context).then do |result|
110
+ Array.wrap(result)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ def self.define_has_many_connection(graph_type, base_model_type, model_type, path, association, object_to_model, options)
117
+ reflection = model_type.reflect_on_association(association)
118
+
119
+ fail ArgumentError.new("Association #{association} wasn't found on model #{model_type.name}") unless reflection
120
+ fail ArgumentError.new("Cannot include #{reflection.macro} association #{association} on model #{model_type.name} with has_many_connection") unless [:has_many].include?(reflection.macro)
121
+
122
+ type_lambda = -> { "#{reflection.klass.name}Graph".constantize.connection_type }
123
+ camel_name = options[:name] || association.to_s.camelize(:lower).to_sym
124
+
125
+ DefinitionHelpers.register_field_metadata(graph_type, camel_name, {
126
+ macro: :has_many_connection,
127
+ macro_type: :association,
128
+ path: path,
129
+ association: association,
130
+ base_model_type: base_model_type,
131
+ model_type: model_type,
132
+ object_to_base_model: object_to_model
133
+ })
134
+
135
+ GraphQL::Relay::Define::AssignConnection.call(graph_type, camel_name, type_lambda) do
136
+ resolve -> (model, args, context) do
137
+ return nil unless model
138
+ return GraphSupport.secure(model.public_send(association), context)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,103 @@
1
+ module GraphQL
2
+ module Models
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
8
+ end
9
+
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]
23
+ else
24
+ types.String
25
+ end
26
+ end
27
+
28
+ def self.get_column(model_type, name)
29
+ col = model_type.columns.detect { |c| c.name == name.to_s }
30
+ raise ArgumentError.new("The attribute #{name} wasn't found on model #{model_type.name}.") unless col
31
+
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
+ })
47
+ end
48
+
49
+ def self.range_to_graphql(value)
50
+ return nil unless value
51
+
52
+ begin
53
+ [value.first, value.last_included]
54
+ rescue TypeError
55
+ [value.first, value.last]
56
+ end
57
+ end
58
+
59
+ # Adds a field to the graph type which is resolved by accessing an attribute on the model. Traverses
60
+ # across has_one associations specified in the path. The resolver returns a promise.
61
+ # @param graph_type The GraphQL::ObjectType that the field is being added to
62
+ # @param model_type The class object for the model that defines the attribute
63
+ # @param path The associations (in order) that need to be loaded, starting from the graph_type's model
64
+ # @param attribute The name of the attribute that is accessed on the target model_type
65
+ def self.define_attribute(graph_type, base_model_type, model_type, path, attribute, object_to_model, options)
66
+ column = get_column(model_type, attribute)
67
+ field_name = options[:name] || column.camel_name
68
+
69
+ DefinitionHelpers.register_field_metadata(graph_type, field_name, {
70
+ macro: :attr,
71
+ macro_type: :attribute,
72
+ path: path,
73
+ attribute: attribute,
74
+ base_model_type: base_model_type,
75
+ model_type: model_type,
76
+ object_to_base_model: object_to_model
77
+ })
78
+
79
+ graph_type.fields[field_name.to_s] = GraphQL::Field.define do
80
+ name field_name.to_s
81
+ type column.graphql_type
82
+ description options[:description] if options.include?(:description)
83
+ deprecation_reason options[:deprecation_reason] if options.include?(:deprecation_reason)
84
+
85
+ resolve -> (model, args, context) do
86
+ return nil unless model
87
+
88
+ if column.is_range
89
+ DefinitionHelpers.range_to_graphql(model.public_send(attribute))
90
+ else
91
+ if model_type.graphql_resolvers.include?(attribute)
92
+ resolve_proc = model_type.graphql_resolvers[attribute]
93
+ model.instance_exec(&resolve_proc)
94
+ else
95
+ model.public_send(attribute)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,117 @@
1
+ require 'ostruct'
2
+
3
+ module GraphQL
4
+ module Models
5
+ module DefinitionHelpers
6
+ def self.types
7
+ GraphQL::Define::TypeDefiner.instance
8
+ end
9
+
10
+ # Returns a promise that will eventually resolve to the model that is at the end of the path
11
+ def self.load_and_traverse(current_model, path, context)
12
+ cache_model(context, current_model)
13
+ return Promise.resolve(current_model) if path.length == 0 || current_model.nil?
14
+
15
+ association = current_model.association(path[0])
16
+
17
+ while path.length > 0 && (association.loaded? || attempt_cache_load(current_model, association, context))
18
+ current_model = association.target
19
+ path = path[1..-1]
20
+ cache_model(context, current_model)
21
+
22
+ return Promise.resolve(current_model) if path.length == 0 || current_model.nil?
23
+
24
+ association = current_model.association(path[0])
25
+ end
26
+
27
+ request = AssociationLoadRequest.new(current_model, path[0], context)
28
+ request.load.then do |next_model|
29
+ next next_model if next_model.blank?
30
+ cache_model(context, next_model)
31
+
32
+ if path.length == 1
33
+ sanity = next_model.is_a?(Array) ? next_model[0] : next_model
34
+ next next_model
35
+ else
36
+ DefinitionHelpers.load_and_traverse(next_model, path[1..-1], context)
37
+ end
38
+ end
39
+ end
40
+
41
+ # Attempts to retrieve the model from the query's cache. If it's found, the association is marked as
42
+ # loaded and the model is added. This only works for belongs_to and has_one associations.
43
+ def self.attempt_cache_load(model, association, context)
44
+ return false unless context
45
+
46
+ reflection = association.reflection
47
+ return false unless [:has_one, :belongs_to].include?(reflection.macro)
48
+
49
+ if reflection.macro == :belongs_to
50
+ target_id = model.send(reflection.foreign_key)
51
+
52
+ # If there isn't an associated model, mark the association loaded and return
53
+ mark_association_loaded(association, nil) and return true if target_id.nil?
54
+
55
+ # If the associated model isn't cached, return false
56
+ target = context.cached_models.detect { |m| m.is_a?(association.klass) && m.id == target_id }
57
+ return false unless target
58
+
59
+ # Found it!
60
+ mark_association_loaded(association, target)
61
+ return true
62
+ else
63
+ target = context.cached_models.detect do |m|
64
+ m.is_a?(association.klass) && m.send(reflection.foreign_key) == model.id && (!reflection.options.include?(:as) || m.send(reflection.type) == model.class.name)
65
+ end
66
+
67
+ return false unless target
68
+
69
+ mark_association_loaded(association, target)
70
+ return true
71
+ end
72
+ end
73
+
74
+ def self.cache_model(context, model)
75
+ return unless context
76
+ context.cached_models.merge(Array.wrap(model))
77
+ end
78
+
79
+ def self.mark_association_loaded(association, target)
80
+ association.loaded!
81
+ association.target = target
82
+ association.set_inverse_instance(target) unless target.nil?
83
+ end
84
+
85
+ def self.traverse_path(base_model, path, context)
86
+ model = base_model
87
+ path.each do |segment|
88
+ return nil unless model
89
+ model = model.public_send(segment)
90
+ end
91
+
92
+ return model
93
+ end
94
+
95
+ # Detects the values that are valid for an attribute by looking at the inclusion validators
96
+ def self.detect_inclusion_values(model_type, attribute)
97
+ # Get all of the inclusion validators
98
+ validators = model_type.validators_on(attribute).select { |v| v.is_a?(ActiveModel::Validations::InclusionValidator) }
99
+
100
+ # Ignore any inclusion validators that are using the 'if' or 'unless' options
101
+ validators = validators.reject { |v| v.options.include?(:if) || v.options.include?(:unless) || v.options[:in].blank? }
102
+ return nil unless validators.any?
103
+ return validators.map { |v| v.options[:in] }.reduce(:&)
104
+ end
105
+
106
+ # Stores metadata about GraphQL fields that are available on this model's GraphQL type.
107
+ # @param metadata Should be a hash that contains information about the field's definition, including :macro and :type
108
+ def self.register_field_metadata(graph_type, field_name, metadata)
109
+ field_name = field_name.to_s
110
+
111
+ field_meta = graph_type.instance_variable_get(:@field_metadata)
112
+ field_meta = graph_type.instance_variable_set(:@field_metadata, {}) unless field_meta
113
+ field_meta[field_name] = OpenStruct.new(metadata).freeze
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,104 @@
1
+ module GraphQL
2
+ module Models
3
+ class Loader < GraphQL::Batch::Loader
4
+ attr_reader :model_class
5
+
6
+ def initialize(model_class)
7
+ @model_class = model_class
8
+ end
9
+
10
+ def perform(load_requests)
11
+
12
+ # Group the requests to load by id into a single relation, and we'll fan it back out after
13
+ # we have the results
14
+
15
+ relations = []
16
+ id_requests = load_requests.select { |r| r.load_type == :id }
17
+ id_relation = model_class.where(id: id_requests.map(&:load_target))
18
+
19
+ relations.push(id_relation) if id_requests.any?
20
+ relations_to_requests = {}
21
+
22
+ # Gather up all of the requests to load a relation, and map the relations back to their requests
23
+ load_requests.select { |r| r.load_type == :relation }.each do |request|
24
+ relation = request.load_target
25
+ relations.push(relation) unless relations.detect { |r| r.object_id == relation.object_id }
26
+ relations_to_requests[relation.object_id] ||= []
27
+ relations_to_requests[relation.object_id].push(request)
28
+ end
29
+
30
+ # We need to build a query that will return all of the rows that match any of the relations.
31
+ # But in addition, we also need to know how that relation sorted them. So we pull any ordering
32
+ # information by adding RANK() columns to the query, and we determine whether that row belonged
33
+ # to the query by adding CASE columns.
34
+
35
+ # Map each relation to a SQL query that selects only the ID column for the rows that match it
36
+ selection_clauses = relations.map do |relation|
37
+ relation.unscope(:select).select(model_class.primary_key).to_sql
38
+ end
39
+
40
+ # Generate a CASE column that will tell us whether the row matches this particular relation
41
+ slicing_columns = relations.each_with_index.map do |relation, index|
42
+ %{ CASE WHEN "#{model_class.table_name}"."#{model_class.primary_key}" IN (#{selection_clauses[index]}) THEN 1 ELSE 0 END AS "in_relation_#{index}" }
43
+ end
44
+
45
+ # For relations that have sorting applied, generate a RANK() column that tells us how the rows are
46
+ # sorted within that relation
47
+ sorting_columns = relations.each_with_index.map do |relation, index|
48
+ arel = relation.arel
49
+ next nil unless arel.orders.any?
50
+
51
+ order_by = arel.orders.map do |expr|
52
+ if expr.is_a?(Arel::Nodes::SqlLiteral)
53
+ expr.to_s
54
+ else
55
+ expr.to_sql
56
+ end
57
+ end
58
+
59
+ %{ RANK() OVER (ORDER BY #{order_by.join(', ')}) AS "sort_relation_#{index}" }
60
+ end
61
+
62
+ sorting_columns.compact!
63
+
64
+ # Build the query that will select any of the rows that match the selection clauses
65
+ main_relation = model_class
66
+ .where("id in ((#{selection_clauses.join(") UNION (")}))")
67
+ .select(%{ "#{model_class.table_name}".* })
68
+
69
+ main_relation = slicing_columns.reduce(main_relation) { |relation, memo| relation.select(memo) }
70
+ main_relation = sorting_columns.reduce(main_relation) { |relation, memo| relation.select(memo) }
71
+
72
+ # Run the query
73
+ result = main_relation.to_a
74
+
75
+ # Now multiplex the results out to all of the relations that had asked for values
76
+ relations.each_with_index do |relation, index|
77
+ slice_col = "in_relation_#{index}"
78
+ sort_col = "sort_relation_#{index}"
79
+
80
+ matching_rows = result.select { |r| r[slice_col] == 1 }.sort_by { |r| r[sort_col] }
81
+
82
+ if relation.object_id == id_relation.object_id
83
+ pk = relation.klass.primary_key
84
+
85
+ id_requests.each do |request|
86
+ row = matching_rows.detect { |r| r[pk] == request.load_target }
87
+ fulfill(request, row)
88
+ end
89
+ else
90
+ relations_to_requests[relation.object_id].each do |request|
91
+ fulfill_request(request, matching_rows)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ def fulfill_request(request, result)
98
+ result = request.ensure_cardinality(result)
99
+ request.fulfilled(result)
100
+ fulfill(request, result)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,23 @@
1
+ class GraphQL::Models::Middleware
2
+ attr_accessor :skip_nil_models
3
+
4
+ def initialize(skip_nil_models = true)
5
+ @skip_nil_models = skip_nil_models
6
+ end
7
+
8
+ def call(graphql_type, object, field_definition, args, context, next_middleware)
9
+ # If this field defines a path, load the associations in the path
10
+ field_info = GraphQL::Models.field_info(graphql_type, field_definition.name)
11
+ return next_middleware.call unless field_info
12
+
13
+ # Convert the core object into the model
14
+ base_model = field_info.object_to_base_model.call(object)
15
+
16
+ GraphQL::Models.load_association(base_model, field_info.path, context).then do |model|
17
+ next nil if model.nil? && @skip_nil_models
18
+
19
+ next_args = [graphql_type, model, field_definition, args, context]
20
+ next_middleware.call(next_args)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ class GraphQL::Query::Context
2
+ def cached_models
3
+ @cached_models ||= Set.new
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ # Monkey patch... there will soon be a PR in graphql-ruby for this functionality,
2
+ # Talked with the gem author (@rmosolgo) and he said it was a good feature, so likely to land soon
3
+ class GraphQL::Schema::MiddlewareChain
4
+ def call(next_arguments = @arguments)
5
+ @arguments = next_arguments
6
+ next_step = steps.shift
7
+ next_middleware = self
8
+ next_step.call(*arguments, next_middleware)
9
+ end
10
+ end
@@ -0,0 +1,120 @@
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)
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)
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
@@ -0,0 +1,22 @@
1
+ module GraphQL
2
+ module Models
3
+ class PromiseRelationConnection < GraphQL::Relay::RelationConnection
4
+ def edges
5
+ # Can't do any optimization if the request is asking for the last X items, since there's
6
+ # no easy way to turn it into a generalized query.
7
+ return super if last
8
+
9
+ relation = sliced_nodes
10
+ limit = [first, last, max_page_size].compact.min
11
+ relation = relation.limit(limit) if first
12
+ request = RelationLoadRequest.new(relation)
13
+
14
+ request.load.then do |models|
15
+ models.map { |m| GraphQL::Relay::Edge.new(m, self) }
16
+ end
17
+ end
18
+ end
19
+
20
+ GraphQL::Relay::BaseConnection.register_connection_implementation(ActiveRecord::Relation, PromiseRelationConnection)
21
+ end
22
+ end
@@ -0,0 +1,52 @@
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)
17
+ DefinitionHelpers.define_attribute(@graph_type, @base_model_type, @model_type, @path, name, @object_to_model, options)
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
@@ -0,0 +1,52 @@
1
+ module GraphQL
2
+ module Models
3
+ class RelationLoadRequest
4
+ attr_reader :relation
5
+
6
+ def initialize(relation)
7
+ @relation = relation
8
+ end
9
+
10
+ ####################################################################
11
+ # Public members that all load requests should implement
12
+ ####################################################################
13
+
14
+ def load_type
15
+ :relation
16
+ end
17
+
18
+ def load_target
19
+ relation
20
+ end
21
+
22
+ # If the value should be an array, make sure it's an array. If it should be a single value, make sure it's single.
23
+ # Passed in result could be a single model or an array of models.
24
+ def ensure_cardinality(result)
25
+ Array.wrap(result)
26
+ end
27
+
28
+ # When the request is fulfilled, this method is called so that it can do whatever caching, etc. is needed
29
+ def fulfilled(result)
30
+ end
31
+
32
+ def load
33
+ loader.load(self)
34
+ end
35
+
36
+ #################################################################
37
+ # Public members specific to a relation load request
38
+ #################################################################
39
+
40
+ def target_class
41
+ relation.klass
42
+ end
43
+
44
+ private
45
+
46
+ def loader
47
+ @loader ||= Loader.for(target_class)
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,5 @@
1
+ module GraphQL
2
+ module Models
3
+ VERSION = "0.3.1"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: graphql-activerecord
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.1
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Foster
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-05-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: graphql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.12.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: graphql-relay
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.8.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.8.0
41
+ description: Build Relay-compatible GraphQL schemas based on ActiveRecord models
42
+ email:
43
+ - theorygeek@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - lib/graphql/activerecord.rb
49
+ - lib/graphql/models/active_record_extension.rb
50
+ - lib/graphql/models/association_load_request.rb
51
+ - lib/graphql/models/backed_by_model.rb
52
+ - lib/graphql/models/definer.rb
53
+ - lib/graphql/models/definition_helpers.rb
54
+ - lib/graphql/models/definition_helpers/associations.rb
55
+ - lib/graphql/models/definition_helpers/attributes.rb
56
+ - lib/graphql/models/loader.rb
57
+ - lib/graphql/models/middleware.rb
58
+ - lib/graphql/models/monkey_patches/graphql_query_context.rb
59
+ - lib/graphql/models/monkey_patches/graphql_schema_middleware_chain.rb
60
+ - lib/graphql/models/object_type.rb
61
+ - lib/graphql/models/promise_relation_connection.rb
62
+ - lib/graphql/models/proxy_block.rb
63
+ - lib/graphql/models/relation_load_request.rb
64
+ - lib/graphql/models/scalar_types.rb
65
+ - lib/graphql/models/version.rb
66
+ homepage: http://github.com/goco-inc/graphql-activerecord
67
+ licenses:
68
+ - MIT
69
+ metadata: {}
70
+ post_install_message:
71
+ rdoc_options: []
72
+ require_paths:
73
+ - lib
74
+ required_ruby_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: 2.1.0
79
+ required_rubygems_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ requirements: []
85
+ rubyforge_project:
86
+ rubygems_version: 2.4.8
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: ActiveRecord helpers for GraphQL + Relay
90
+ test_files: []