graphiti_graphql 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +144 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +14 -0
- data/bin/graphiti +29 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/standardrb +29 -0
- data/config/routes.rb +6 -0
- data/graphiti_graphql.gemspec +45 -0
- data/lib/graphiti_graphql.rb +71 -0
- data/lib/graphiti_graphql/engine.rb +89 -0
- data/lib/graphiti_graphql/errors.rb +5 -0
- data/lib/graphiti_graphql/federation.rb +263 -0
- data/lib/graphiti_graphql/graphiti_schema/resource.rb +117 -0
- data/lib/graphiti_graphql/graphiti_schema/sideload.rb +56 -0
- data/lib/graphiti_graphql/graphiti_schema/wrapper.rb +36 -0
- data/lib/graphiti_graphql/runner.rb +313 -0
- data/lib/graphiti_graphql/schema.rb +538 -0
- data/lib/graphiti_graphql/util.rb +27 -0
- data/lib/graphiti_graphql/version.rb +3 -0
- metadata +260 -0
@@ -0,0 +1,263 @@
|
|
1
|
+
begin
|
2
|
+
require "apollo-federation"
|
3
|
+
rescue LoadError
|
4
|
+
raise "You must add the 'apollo-federation' gem to use GraphitiGraphQL federation"
|
5
|
+
end
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "graphql/batch"
|
9
|
+
rescue LoadError
|
10
|
+
raise "You must add the 'graphql-batch' gem to use GraphitiGraphQL federation"
|
11
|
+
end
|
12
|
+
|
13
|
+
# We don't want to add these as dependencies,
|
14
|
+
# but do need to check things don't break
|
15
|
+
if Gem::Version.new(ApolloFederation::VERSION) >= Gem::Version.new('2.0.0')
|
16
|
+
raise "graphiti_graphql federation is incompatible with apollo-federation >= 2"
|
17
|
+
end
|
18
|
+
|
19
|
+
if Gem::Version.new(GraphQL::Batch::VERSION) >= Gem::Version.new('1.0.0')
|
20
|
+
raise "graphiti_graphql federation is incompatible with graphql-batch >= 1"
|
21
|
+
end
|
22
|
+
|
23
|
+
require "graphiti_graphql"
|
24
|
+
|
25
|
+
module GraphitiGraphQL
|
26
|
+
module Federation
|
27
|
+
|
28
|
+
def self.external_resources
|
29
|
+
@external_resources ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.clear!
|
33
|
+
@external_resources = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.setup!
|
37
|
+
Graphiti::Resource.send(:include, ResourceDSL)
|
38
|
+
schema = GraphitiGraphQL::Schema
|
39
|
+
schema.base_field = Class.new(schema.base_field) do
|
40
|
+
include ApolloFederation::Field
|
41
|
+
end
|
42
|
+
schema.base_object = Class.new(schema.base_object) do
|
43
|
+
include ApolloFederation::Object
|
44
|
+
end
|
45
|
+
schema.base_object.field_class(schema.base_field)
|
46
|
+
schema.base_interface = Module.new do
|
47
|
+
include GraphQL::Schema::Interface
|
48
|
+
include ApolloFederation::Interface
|
49
|
+
end
|
50
|
+
schema.base_interface.field_class(schema.base_field)
|
51
|
+
GraphitiGraphQL::Schema.federation = true
|
52
|
+
end
|
53
|
+
|
54
|
+
class HasManyLoader < GraphQL::Batch::Loader
|
55
|
+
def initialize(resource_class, params, foreign_key)
|
56
|
+
@resource_class = resource_class
|
57
|
+
@params = params
|
58
|
+
@foreign_key = foreign_key
|
59
|
+
end
|
60
|
+
|
61
|
+
def perform(ids)
|
62
|
+
@params[:filter] ||= {}
|
63
|
+
@params[:filter].merge!(@foreign_key => { eq: ids.join(",") })
|
64
|
+
|
65
|
+
if ids.length > 1 && @params[:page]
|
66
|
+
raise Graphiti::Errors::UnsupportedPagination
|
67
|
+
elsif !@params[:page]
|
68
|
+
@params[:page] = { size: 999 }
|
69
|
+
end
|
70
|
+
|
71
|
+
Util.with_gql_context do
|
72
|
+
records = @resource_class.all(@params).as_json[:data]
|
73
|
+
fk = ->(record) { record[@foreign_key].to_s }
|
74
|
+
map = records.group_by(&fk)
|
75
|
+
ids.each do |id|
|
76
|
+
fulfill(id, (map[id] || []))
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class BelongsToLoader < GraphQL::Batch::Loader
|
83
|
+
def initialize(resource_class, fields)
|
84
|
+
@resource_class = resource_class
|
85
|
+
@fields = fields
|
86
|
+
end
|
87
|
+
|
88
|
+
def perform(ids)
|
89
|
+
Util.with_gql_context do
|
90
|
+
params = { filter: { id: { eq: ids.join(",") } } }
|
91
|
+
params[:fields] = { @resource_class.type => @fields.join(",") }
|
92
|
+
records = @resource_class.all(params).as_json[:data]
|
93
|
+
pk = ->(record) { record[:id].to_s }
|
94
|
+
map = records.index_by(&pk)
|
95
|
+
ids.each { |id| fulfill(id, map[id]) }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ExternalRelationship
|
101
|
+
attr_reader :name, :local_resource_class, :foreign_key
|
102
|
+
|
103
|
+
def initialize(kind, name, local_resource_class, foreign_key)
|
104
|
+
@kind = kind
|
105
|
+
@name = name
|
106
|
+
@local_resource_class = local_resource_class
|
107
|
+
@foreign_key = foreign_key
|
108
|
+
end
|
109
|
+
|
110
|
+
def has_many?
|
111
|
+
@kind == :has_many
|
112
|
+
end
|
113
|
+
|
114
|
+
def belongs_to?
|
115
|
+
@kind == :belongs_to
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class ExternalResource
|
120
|
+
attr_reader :type_name, :relationships
|
121
|
+
|
122
|
+
def initialize(type_name)
|
123
|
+
@type_name = type_name
|
124
|
+
@relationships = {}
|
125
|
+
end
|
126
|
+
|
127
|
+
def add_relationship(
|
128
|
+
kind,
|
129
|
+
name,
|
130
|
+
local_resource_class,
|
131
|
+
foreign_key
|
132
|
+
)
|
133
|
+
@relationships[name] = ExternalRelationship
|
134
|
+
.new(kind, name, local_resource_class, foreign_key)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class TypeProxy
|
139
|
+
def initialize(caller, type_name)
|
140
|
+
@caller = caller
|
141
|
+
@type_name = type_name
|
142
|
+
end
|
143
|
+
|
144
|
+
def has_many(relationship_name, foreign_key: nil)
|
145
|
+
@caller.federated_has_many relationship_name,
|
146
|
+
type: @type_name,
|
147
|
+
foreign_key: foreign_key
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
module ResourceDSL
|
152
|
+
extend ActiveSupport::Concern
|
153
|
+
|
154
|
+
class_methods do
|
155
|
+
def federated_type(type_name)
|
156
|
+
TypeProxy.new(self, type_name)
|
157
|
+
end
|
158
|
+
|
159
|
+
# TODO: raise error if belongs_to doesn't have corresponding filter (on schema gen)
|
160
|
+
# TODO: hang these on the resource classes themselves
|
161
|
+
def federated_has_many(name, type:, foreign_key: nil)
|
162
|
+
foreign_key ||= :"#{type.underscore}_id"
|
163
|
+
resource = GraphitiGraphQL::Federation.external_resources[type] ||=
|
164
|
+
ExternalResource.new(type)
|
165
|
+
resource.add_relationship(:has_many, name, self, foreign_key)
|
166
|
+
|
167
|
+
attribute = attributes.find do |name, config|
|
168
|
+
name.to_sym == foreign_key && !!config[:readable] && !!config[:filterable]
|
169
|
+
end
|
170
|
+
has_filter = filters.key?(foreign_key)
|
171
|
+
if !attribute && !has_filter
|
172
|
+
attribute foreign_key, :integer,
|
173
|
+
only: [:readable, :filterable],
|
174
|
+
schema: false,
|
175
|
+
readable: :gql?,
|
176
|
+
filterable: :gql?
|
177
|
+
elsif has_filter && !attribute
|
178
|
+
prior = filters[foreign_key]
|
179
|
+
attribute foreign_key, prior[:type],
|
180
|
+
only: [:readable, :filterable],
|
181
|
+
schema: false,
|
182
|
+
readable: :gql?
|
183
|
+
filters[foreign_key] = prior
|
184
|
+
elsif attribute && !has_filter
|
185
|
+
filter foreign_key, attribute[:type]
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def federated_belongs_to(name, type: nil, foreign_key: nil)
|
190
|
+
type ||= name.to_s.camelize
|
191
|
+
foreign_key ||= :"#{name.to_s.underscore}_id"
|
192
|
+
resource = GraphitiGraphQL::Federation.external_resources[type] ||=
|
193
|
+
ExternalResource.new(type)
|
194
|
+
resource.add_relationship(:belongs_to, name, self, foreign_key)
|
195
|
+
|
196
|
+
attribute name, :hash, readable: :gql?, only: [:readable], schema: false do
|
197
|
+
fk = if prc = self.class.attribute_blocks[foreign_key]
|
198
|
+
instance_eval(&prc)
|
199
|
+
else
|
200
|
+
@object.send(foreign_key)
|
201
|
+
end
|
202
|
+
{
|
203
|
+
__typename: type,
|
204
|
+
id: fk.to_s
|
205
|
+
}
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def gql?
|
211
|
+
Graphiti.context[:graphql]
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Hacky sack!
|
218
|
+
# All we're doing here is adding extras: [:lookahead] to the _entities field
|
219
|
+
# And passing to to the .resolve_reference method when arity is 3
|
220
|
+
# This way we can request only fields the user wants when resolving the reference
|
221
|
+
# Important because we blow up when a field is guarded, and the guard fails
|
222
|
+
ApolloFederation::EntitiesField::ClassMethods.module_eval do
|
223
|
+
alias_method :define_entities_field_without_override, :define_entities_field
|
224
|
+
def define_entities_field(*args)
|
225
|
+
result = define_entities_field_without_override(*args)
|
226
|
+
extras = fields["_entities"].extras
|
227
|
+
extras |= [:lookahead]
|
228
|
+
fields["_entities"].instance_variable_set(:@extras, extras)
|
229
|
+
result
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
module EntitiesFieldOverride
|
234
|
+
def _entities(representations:, lookahead:) # accept the lookahead as argument
|
235
|
+
representations.map do |reference|
|
236
|
+
typename = reference[:__typename]
|
237
|
+
type = context.warden.get_type(typename)
|
238
|
+
if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
|
239
|
+
raise "The _entities resolver tried to load an entity for type \"#{typename}\"," \
|
240
|
+
' but no object type of that name was found in the schema'
|
241
|
+
end
|
242
|
+
|
243
|
+
type_class = type.is_a?(GraphQL::ObjectType) ? type.metadata[:type_class] : type
|
244
|
+
if type_class.respond_to?(:resolve_reference)
|
245
|
+
meth = type_class.method(:resolve_reference)
|
246
|
+
# ** THIS IS OUR EDIT **
|
247
|
+
result = if meth.arity == 3
|
248
|
+
type_class.resolve_reference(reference, context, lookahead)
|
249
|
+
else
|
250
|
+
type_class.resolve_reference(reference, context)
|
251
|
+
end
|
252
|
+
else
|
253
|
+
result = reference
|
254
|
+
end
|
255
|
+
|
256
|
+
context.schema.after_lazy(result) do |resolved_value|
|
257
|
+
context[resolved_value] = type
|
258
|
+
resolved_value
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
ApolloFederation::EntitiesField.send :prepend, EntitiesFieldOverride
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module GraphitiSchema
|
3
|
+
class Resource
|
4
|
+
attr_reader :schema, :config
|
5
|
+
|
6
|
+
def self.gql_name(name)
|
7
|
+
Graphiti::Util::Class.graphql_type_name(name)
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(schema, config)
|
11
|
+
@schema = schema
|
12
|
+
@config = config
|
13
|
+
end
|
14
|
+
|
15
|
+
def graphql_class_name(allow_interface = true)
|
16
|
+
class_name = self.class.gql_name(name)
|
17
|
+
if allow_interface
|
18
|
+
if polymorphic? && !children.map(&:name).include?(name)
|
19
|
+
class_name = "I#{class_name}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def sideloads
|
26
|
+
@sideloads ||= {}.tap do |sideloads|
|
27
|
+
config[:relationships].each_pair do |k, v|
|
28
|
+
sideload = Sideload.new(schema, v)
|
29
|
+
sideload.name = k
|
30
|
+
sideloads[k] = sideload
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def related_resource(relationship_name)
|
36
|
+
resource_name = relationships[relationship_name][:resource]
|
37
|
+
schema.get_resource(resource_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def pbt?(name)
|
41
|
+
relationships[name][:type] == "polymorphic_belongs_to"
|
42
|
+
end
|
43
|
+
|
44
|
+
def polymorphic?
|
45
|
+
!!config[:polymorphic]
|
46
|
+
end
|
47
|
+
|
48
|
+
def children
|
49
|
+
config[:children].map do |name|
|
50
|
+
schema.get_resource(name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def remote_url
|
55
|
+
config[:remote]
|
56
|
+
end
|
57
|
+
|
58
|
+
def remote?
|
59
|
+
!!config[:remote]
|
60
|
+
end
|
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
|
+
def name
|
71
|
+
config[:name]
|
72
|
+
end
|
73
|
+
|
74
|
+
def type
|
75
|
+
config[:type]
|
76
|
+
end
|
77
|
+
|
78
|
+
def graphql_entrypoint
|
79
|
+
config[:graphql_entrypoint]
|
80
|
+
end
|
81
|
+
|
82
|
+
def sorts
|
83
|
+
config[:sorts]
|
84
|
+
end
|
85
|
+
|
86
|
+
def filters
|
87
|
+
config[:filters]
|
88
|
+
end
|
89
|
+
|
90
|
+
def relationships
|
91
|
+
config[:relationships]
|
92
|
+
end
|
93
|
+
|
94
|
+
def extra_attributes
|
95
|
+
config[:extra_attributes]
|
96
|
+
end
|
97
|
+
|
98
|
+
def attributes
|
99
|
+
config[:attributes]
|
100
|
+
end
|
101
|
+
|
102
|
+
def all_attributes
|
103
|
+
attributes.merge(extra_attributes)
|
104
|
+
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
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module GraphitiSchema
|
3
|
+
class Sideload
|
4
|
+
attr_reader :config, :schema
|
5
|
+
attr_accessor :name
|
6
|
+
|
7
|
+
def initialize(schema, config)
|
8
|
+
@config = config
|
9
|
+
@schema = schema
|
10
|
+
end
|
11
|
+
|
12
|
+
def graphql_class_name
|
13
|
+
if type == :polymorphic_belongs_to
|
14
|
+
parent_resource.graphql_class_name
|
15
|
+
else
|
16
|
+
resource.graphql_class_name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_many?
|
21
|
+
[:has_many, :many_to_many].include?(type)
|
22
|
+
end
|
23
|
+
|
24
|
+
def type
|
25
|
+
config[:type].to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
def resource_name
|
29
|
+
config[:resource]
|
30
|
+
end
|
31
|
+
|
32
|
+
def resource
|
33
|
+
schema.get_resource(resource_name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def remote?
|
37
|
+
resources = child_resources? ? child_resources : [resource]
|
38
|
+
resources.any?(&:remote?)
|
39
|
+
end
|
40
|
+
|
41
|
+
def parent_resource
|
42
|
+
schema.get_resource(config[:parent_resource])
|
43
|
+
end
|
44
|
+
|
45
|
+
def child_resources?
|
46
|
+
!!config[:resources]
|
47
|
+
end
|
48
|
+
|
49
|
+
def child_resources
|
50
|
+
config[:resources].map do |name|
|
51
|
+
schema.get_resource(name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
module GraphitiSchema
|
3
|
+
class Wrapper
|
4
|
+
attr_reader :schema
|
5
|
+
|
6
|
+
def initialize(schema)
|
7
|
+
@schema = schema
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_resource(name)
|
11
|
+
config = schema[:resources].find { |r| r[:name] == name }
|
12
|
+
raise "Could not find resource #{name} in schema" unless config
|
13
|
+
Resource.new(self, schema[:resources].find { |r| r[:name] == name })
|
14
|
+
end
|
15
|
+
|
16
|
+
def resources
|
17
|
+
schema[:resources].map { |r| get_resource(r[:name]) }
|
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
|
+
end
|
35
|
+
end
|
36
|
+
end
|