graphiti_graphql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module GraphitiGraphQL
2
+ module Errors
3
+ class Base < StandardError; end
4
+ end
5
+ end
@@ -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