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