graphiti_graphql 0.1.1 → 0.1.2
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 +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -4
- data/README.md +139 -15
- data/graphiti_graphql.gemspec +1 -0
- data/lib/graphiti_graphql.rb +47 -34
- data/lib/graphiti_graphql/engine.rb +2 -12
- data/lib/graphiti_graphql/federation.rb +12 -220
- data/lib/graphiti_graphql/federation/apollo_federation_override.rb +54 -0
- data/lib/graphiti_graphql/federation/federated_relationship.rb +26 -0
- data/lib/graphiti_graphql/federation/federated_resource.rb +26 -0
- data/lib/graphiti_graphql/federation/loaders/belongs_to.rb +22 -0
- data/lib/graphiti_graphql/federation/loaders/has_many.rb +33 -0
- data/lib/graphiti_graphql/federation/resource_dsl.rb +89 -0
- data/lib/graphiti_graphql/federation/schema_decorator.rb +150 -0
- data/lib/graphiti_graphql/graphiti_schema/resource.rb +0 -18
- data/lib/graphiti_graphql/graphiti_schema/wrapper.rb +0 -15
- data/lib/graphiti_graphql/runner.rb +15 -25
- data/lib/graphiti_graphql/schema.rb +50 -160
- data/lib/graphiti_graphql/util.rb +1 -1
- data/lib/graphiti_graphql/version.rb +1 -1
- metadata +23 -2
@@ -0,0 +1,54 @@
|
|
1
|
+
# Hacky sack!
|
2
|
+
# All we're doing here is adding extras: [:lookahead] to the _entities field
|
3
|
+
# And passing to to the .resolve_reference method when arity is 3
|
4
|
+
# This way we can request only fields the user wants when resolving the reference
|
5
|
+
# Important because we blow up when a field is guarded, and the guard fails
|
6
|
+
ApolloFederation::EntitiesField::ClassMethods.module_eval do
|
7
|
+
alias_method :define_entities_field_without_override, :define_entities_field
|
8
|
+
def define_entities_field(*args)
|
9
|
+
result = define_entities_field_without_override(*args)
|
10
|
+
extras = fields["_entities"].extras
|
11
|
+
extras |= [:lookahead]
|
12
|
+
fields["_entities"].instance_variable_set(:@extras, extras)
|
13
|
+
result
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module GraphitiGraphQL
|
18
|
+
module Federation
|
19
|
+
module EntitiesFieldOverride
|
20
|
+
# accept the lookahead as argument
|
21
|
+
def _entities(representations:, lookahead:)
|
22
|
+
representations.map do |reference|
|
23
|
+
typename = reference[:__typename]
|
24
|
+
type = context.warden.get_type(typename)
|
25
|
+
if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
|
26
|
+
raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \
|
27
|
+
" but no object type of that name was found in the schema"
|
28
|
+
end
|
29
|
+
|
30
|
+
type_class = type.is_a?(GraphQL::ObjectType) ? type.metadata[:type_class] : type
|
31
|
+
if type_class.respond_to?(:resolve_reference)
|
32
|
+
meth = type_class.method(:resolve_reference)
|
33
|
+
# ** THIS IS OUR EDIT **
|
34
|
+
result = if meth.arity == 3
|
35
|
+
type_class.resolve_reference(reference, context, lookahead)
|
36
|
+
else
|
37
|
+
type_class.resolve_reference(reference, context)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
result = reference
|
41
|
+
end
|
42
|
+
|
43
|
+
context.schema.after_lazy(result) do |resolved_value|
|
44
|
+
context[resolved_value] = type
|
45
|
+
resolved_value
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ApolloFederation::EntitiesField.send :prepend,
|
54
|
+
GraphitiGraphQL::Federation::EntitiesFieldOverride
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
class FederatedRelationship
|
4
|
+
attr_reader :name, :local_resource_class, :foreign_key, :params_block
|
5
|
+
|
6
|
+
def initialize(kind, name, local_resource_class, foreign_key)
|
7
|
+
@kind = kind
|
8
|
+
@name = name
|
9
|
+
@local_resource_class = local_resource_class
|
10
|
+
@foreign_key = foreign_key
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_many?
|
14
|
+
@kind == :has_many
|
15
|
+
end
|
16
|
+
|
17
|
+
def belongs_to?
|
18
|
+
@kind == :belongs_to
|
19
|
+
end
|
20
|
+
|
21
|
+
def params(&blk)
|
22
|
+
@params_block = blk
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
class FederatedResource
|
4
|
+
attr_reader :type_name, :relationships
|
5
|
+
|
6
|
+
def initialize(type_name)
|
7
|
+
@type_name = type_name
|
8
|
+
@relationships = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_relationship(
|
12
|
+
kind,
|
13
|
+
name,
|
14
|
+
local_resource_class,
|
15
|
+
foreign_key,
|
16
|
+
&blk
|
17
|
+
)
|
18
|
+
@relationships[name] = FederatedRelationship
|
19
|
+
.new(kind, name, local_resource_class, foreign_key)
|
20
|
+
if blk
|
21
|
+
@relationships[name].instance_eval(&blk)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
module Loaders
|
4
|
+
class BelongsTo < GraphQL::Batch::Loader
|
5
|
+
def initialize(resource_class, fields)
|
6
|
+
@resource_class = resource_class
|
7
|
+
@fields = fields
|
8
|
+
end
|
9
|
+
|
10
|
+
def perform(ids)
|
11
|
+
Util.with_gql_context do
|
12
|
+
params = {filter: {id: {eq: ids.join(",")}}}
|
13
|
+
params[:fields] = {@resource_class.type => @fields.join(",")}
|
14
|
+
records = @resource_class.all(params).as_json[:data]
|
15
|
+
map = records.index_by { |record| record[:id].to_s }
|
16
|
+
ids.each { |id| fulfill(id, map[id]) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
module Loaders
|
4
|
+
class HasMany < GraphQL::Batch::Loader
|
5
|
+
def initialize(federated_relationship, params)
|
6
|
+
@federated_relationship = federated_relationship
|
7
|
+
@resource_class = federated_relationship.local_resource_class
|
8
|
+
@params = params
|
9
|
+
@foreign_key = federated_relationship.foreign_key
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform(ids)
|
13
|
+
@params[:filter] ||= {}
|
14
|
+
@params[:filter][@foreign_key] = {eq: ids.join(",")}
|
15
|
+
|
16
|
+
@federated_relationship.params_block&.call(@params)
|
17
|
+
|
18
|
+
if ids.length > 1 && @params[:page]
|
19
|
+
raise Graphiti::Errors::UnsupportedPagination
|
20
|
+
elsif !@params[:page]
|
21
|
+
@params[:page] = {size: 999}
|
22
|
+
end
|
23
|
+
|
24
|
+
Util.with_gql_context do
|
25
|
+
records = @resource_class.all(@params).as_json[:data]
|
26
|
+
map = records.group_by { |record| record[@foreign_key].to_s}
|
27
|
+
ids.each { |id| fulfill(id, (map[id] || [])) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
module ResourceDSL
|
4
|
+
class TypeProxy
|
5
|
+
def initialize(caller, type_name)
|
6
|
+
@caller = caller
|
7
|
+
@type_name = type_name
|
8
|
+
end
|
9
|
+
|
10
|
+
def has_many(relationship_name, foreign_key: nil, &blk)
|
11
|
+
@caller.federated_has_many relationship_name,
|
12
|
+
type: @type_name,
|
13
|
+
foreign_key: foreign_key,
|
14
|
+
&blk
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
extend ActiveSupport::Concern
|
19
|
+
|
20
|
+
class_methods do
|
21
|
+
# Sugar around federated_has_many
|
22
|
+
def federated_type(type_name)
|
23
|
+
TypeProxy.new(self, type_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add to Graphiti::Resource config as normal
|
27
|
+
# Helpful for inheritance + testing
|
28
|
+
def federated_resources
|
29
|
+
config[:federated_resources] ||= []
|
30
|
+
end
|
31
|
+
|
32
|
+
# * Add to the list of external graphql-ruby types we need in schema
|
33
|
+
# * Add a readable and filterable FK, without clobbering pre-existing
|
34
|
+
def federated_has_many(name, type:, foreign_key: nil, &blk)
|
35
|
+
foreign_key ||= :"#{type.underscore}_id"
|
36
|
+
resource = FederatedResource.new(type)
|
37
|
+
federated_resources << resource
|
38
|
+
resource.add_relationship(:has_many, name, self, foreign_key, &blk)
|
39
|
+
|
40
|
+
attribute = attributes.find { |name, config|
|
41
|
+
name.to_sym == foreign_key &&
|
42
|
+
!!config[:readable] &&
|
43
|
+
!!config[:filterable]
|
44
|
+
}
|
45
|
+
has_filter = filters.key?(foreign_key)
|
46
|
+
if !attribute && !has_filter
|
47
|
+
attribute foreign_key, :integer,
|
48
|
+
only: [:readable, :filterable],
|
49
|
+
schema: false,
|
50
|
+
readable: :gql?,
|
51
|
+
filterable: :gql?
|
52
|
+
elsif has_filter && !attribute
|
53
|
+
prior = filters[foreign_key]
|
54
|
+
attribute foreign_key, prior[:type],
|
55
|
+
only: [:readable, :filterable],
|
56
|
+
schema: false,
|
57
|
+
readable: :gql?
|
58
|
+
filters[foreign_key] = prior
|
59
|
+
elsif attribute && !has_filter
|
60
|
+
filter foreign_key, attribute[:type]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# * Add to the list of external graphql-ruby types we need in schema
|
65
|
+
# * Add a gql-specific attribute to the serializer that gives apollo
|
66
|
+
# the representation it needs.
|
67
|
+
def federated_belongs_to(name, type: nil, foreign_key: nil)
|
68
|
+
type ||= name.to_s.camelize
|
69
|
+
foreign_key ||= :"#{name.to_s.underscore}_id"
|
70
|
+
resource = FederatedResource.new(type)
|
71
|
+
federated_resources << resource
|
72
|
+
resource.add_relationship(:belongs_to, name, self, foreign_key)
|
73
|
+
|
74
|
+
opts = {readable: :gql?, only: [:readable], schema: false}
|
75
|
+
attribute name, :hash, opts do
|
76
|
+
prc = self.class.attribute_blocks[foreign_key]
|
77
|
+
fk = prc ? instance_eval(&prc) : @object.send(foreign_key)
|
78
|
+
{__typename: type, id: fk.to_s}
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Certain attributes should only work in GQL context
|
84
|
+
def gql?
|
85
|
+
Graphiti.context[:graphql]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module Federation
|
3
|
+
class SchemaDecorator
|
4
|
+
def self.decorate(schema)
|
5
|
+
new(schema).decorate
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(schema)
|
9
|
+
@schema = schema
|
10
|
+
end
|
11
|
+
|
12
|
+
def decorate
|
13
|
+
@schema.schema.send(:include, ApolloFederation::Schema)
|
14
|
+
# NB if we add mutation support, make sure this is applied after
|
15
|
+
@schema.schema.use(GraphQL::Batch)
|
16
|
+
add_resolve_reference
|
17
|
+
add_federated_resources
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add to all local resource types
|
21
|
+
# This is if a remote federated resource belongs_to a local resource
|
22
|
+
def add_resolve_reference
|
23
|
+
@schema.type_registry.each_pair do |name, config|
|
24
|
+
if config[:resource]
|
25
|
+
local_type = config[:type]
|
26
|
+
local_type.key(fields: "id") if local_type.respond_to?(:key)
|
27
|
+
local_resource = Graphiti.resources
|
28
|
+
.find { |r| r.name == config[:resource] }
|
29
|
+
# TODO: maybe turn off the graphiti debug for these?
|
30
|
+
local_type.define_singleton_method :resolve_reference do |reference, context, lookahead|
|
31
|
+
Federation::Loaders::BelongsTo
|
32
|
+
.for(local_resource, lookahead.selections.map(&:name))
|
33
|
+
.load(reference[:id])
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def federated_resources
|
40
|
+
federated = []
|
41
|
+
Graphiti.resources.each do |r|
|
42
|
+
federated |= (r.config[:federated_resources] || [])
|
43
|
+
end
|
44
|
+
federated
|
45
|
+
end
|
46
|
+
|
47
|
+
def type_registry
|
48
|
+
@schema.type_registry
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_federated_resources
|
52
|
+
each_federated_resource do |type_class, federated_resource|
|
53
|
+
federated_resource.relationships.each_pair do |name, relationship|
|
54
|
+
if relationship.has_many?
|
55
|
+
define_federated_has_many(type_class, relationship)
|
56
|
+
elsif relationship.belongs_to?
|
57
|
+
define_federated_belongs_to(federated_resource, relationship)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# NB: test already registered bc 2 things have same relationship
|
64
|
+
def each_federated_resource
|
65
|
+
federated_resources.each do |federated_resource|
|
66
|
+
pre_registered = !!type_registry[federated_resource.type_name]
|
67
|
+
type_class = if pre_registered
|
68
|
+
type_registry[federated_resource.type_name][:type]
|
69
|
+
else
|
70
|
+
add_federated_resource_type(federated_resource.type_name)
|
71
|
+
end
|
72
|
+
|
73
|
+
yield type_class, federated_resource
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_federated_resource_type(klass_name)
|
78
|
+
federated_type = Class.new(@schema.class.base_object)
|
79
|
+
federated_type.graphql_name klass_name
|
80
|
+
federated_type.key(fields: "id")
|
81
|
+
federated_type.extend_type
|
82
|
+
federated_type.field :id, String, null: false, external: true
|
83
|
+
federated_type.class_eval do
|
84
|
+
def self.resolve_reference(reference, _context, _lookup)
|
85
|
+
reference
|
86
|
+
end
|
87
|
+
end
|
88
|
+
# NB must be registered before processing relationships
|
89
|
+
type_registry[klass_name] = {type: federated_type}
|
90
|
+
federated_type
|
91
|
+
end
|
92
|
+
|
93
|
+
def define_federated_has_many(type_class, relationship)
|
94
|
+
local_name = GraphitiGraphQL::GraphitiSchema::Resource
|
95
|
+
.gql_name(relationship.local_resource_class.name)
|
96
|
+
local_type = type_registry[local_name][:type]
|
97
|
+
local_resource_name = type_registry[local_name][:resource]
|
98
|
+
local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
|
99
|
+
|
100
|
+
local_interface = type_registry["I#{local_name}"]
|
101
|
+
best_type = local_interface ? local_interface[:type] : local_type
|
102
|
+
|
103
|
+
field = type_class.field relationship.name,
|
104
|
+
[best_type],
|
105
|
+
null: false,
|
106
|
+
extras: [:lookahead]
|
107
|
+
|
108
|
+
@schema.send :define_arguments_for_sideload_field,
|
109
|
+
field, @schema.graphiti_schema.get_resource(local_resource_name)
|
110
|
+
type_class.define_method relationship.name do |lookahead:, **arguments|
|
111
|
+
# TODO test params...do version of sort with array/symbol keys and plain string
|
112
|
+
params = arguments.as_json
|
113
|
+
.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
114
|
+
selections = lookahead.selections.map(&:name)
|
115
|
+
selections << relationship.foreign_key
|
116
|
+
selections << :_type # polymorphism
|
117
|
+
params[:fields] = {local_resource.type => selections.join(",")}
|
118
|
+
|
119
|
+
if (sort = Util.parse_sort(params[:sort]))
|
120
|
+
params[:sort] = sort
|
121
|
+
end
|
122
|
+
|
123
|
+
Federation::Loaders::HasMany
|
124
|
+
.for(relationship, params)
|
125
|
+
.load(object[:id])
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def define_federated_belongs_to(federated_resource, relationship)
|
130
|
+
type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
|
131
|
+
local_type = type_registry[type_name][:type]
|
132
|
+
|
133
|
+
# Todo maybe better way here
|
134
|
+
interface = type_registry["I#{type_name}"]
|
135
|
+
|
136
|
+
local_type = interface[:type] if interface
|
137
|
+
local_types = [local_type]
|
138
|
+
if interface
|
139
|
+
local_types |= interface[:implementers]
|
140
|
+
end
|
141
|
+
|
142
|
+
local_types.each do |local|
|
143
|
+
local.field relationship.name,
|
144
|
+
type_registry[federated_resource.type_name][:type],
|
145
|
+
null: true
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -59,14 +59,6 @@ module GraphitiGraphQL
|
|
59
59
|
!!config[:remote]
|
60
60
|
end
|
61
61
|
|
62
|
-
def fetch_remote_schema!
|
63
|
-
parts = remote_url.split("/")
|
64
|
-
parts.pop
|
65
|
-
url = "#{parts.join("/")}/vandal/schema.json"
|
66
|
-
response = faraday.get(url)
|
67
|
-
JSON.parse(response.body).deep_symbolize_keys
|
68
|
-
end
|
69
|
-
|
70
62
|
def name
|
71
63
|
config[:name]
|
72
64
|
end
|
@@ -102,16 +94,6 @@ module GraphitiGraphQL
|
|
102
94
|
def all_attributes
|
103
95
|
attributes.merge(extra_attributes)
|
104
96
|
end
|
105
|
-
|
106
|
-
private
|
107
|
-
|
108
|
-
def faraday
|
109
|
-
if defined?(Faraday)
|
110
|
-
Faraday
|
111
|
-
else
|
112
|
-
raise "Faraday not defined. Please require the 'faraday' gem to use remote resources"
|
113
|
-
end
|
114
|
-
end
|
115
97
|
end
|
116
98
|
end
|
117
99
|
end
|
@@ -16,21 +16,6 @@ module GraphitiGraphQL
|
|
16
16
|
def resources
|
17
17
|
schema[:resources].map { |r| get_resource(r[:name]) }
|
18
18
|
end
|
19
|
-
|
20
|
-
# TODO some work here, dupes, refer back, etc
|
21
|
-
def merge_remotes!
|
22
|
-
resources.select(&:remote?).each do |resource|
|
23
|
-
remote_schema = resource.fetch_remote_schema!
|
24
|
-
remote_schema[:resources].each do |remote_config|
|
25
|
-
unless resources.map(&:name).include?(remote_config[:name])
|
26
|
-
remote_config[:name] = resource.name
|
27
|
-
schema[:resources].reject! { |r| r[:name] == resource.name }
|
28
|
-
schema[:resources] << remote_config
|
29
|
-
schema
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
19
|
end
|
35
20
|
end
|
36
21
|
end
|