graphql-activerecord 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/graphql/activerecord.rb +54 -0
- data/lib/graphql/models/active_record_extension.rb +63 -0
- data/lib/graphql/models/association_load_request.rb +97 -0
- data/lib/graphql/models/backed_by_model.rb +59 -0
- data/lib/graphql/models/definer.rb +26 -0
- data/lib/graphql/models/definition_helpers/associations.rb +144 -0
- data/lib/graphql/models/definition_helpers/attributes.rb +103 -0
- data/lib/graphql/models/definition_helpers.rb +117 -0
- data/lib/graphql/models/loader.rb +104 -0
- data/lib/graphql/models/middleware.rb +23 -0
- data/lib/graphql/models/monkey_patches/graphql_query_context.rb +5 -0
- data/lib/graphql/models/monkey_patches/graphql_schema_middleware_chain.rb +10 -0
- data/lib/graphql/models/object_type.rb +120 -0
- data/lib/graphql/models/promise_relation_connection.rb +22 -0
- data/lib/graphql/models/proxy_block.rb +52 -0
- data/lib/graphql/models/relation_load_request.rb +52 -0
- data/lib/graphql/models/scalar_types.rb +15 -0
- data/lib/graphql/models/version.rb +5 -0
- metadata +90 -0
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,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
|
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: []
|