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.
@@ -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