graphiti_graphql 0.1.0
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/.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
|