graphql-activerecord 0.3.1

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 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: []