graphiti_graphql 0.1.0 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,38 @@
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
+
25
+ def polymorphic?
26
+ @type_name.is_a?(Hash)
27
+ end
28
+
29
+ def klass_name
30
+ if polymorphic?
31
+ "I#{@relationships.keys[0].to_s.camelize}"
32
+ else
33
+ @type_name
34
+ end
35
+ end
36
+ end
37
+ end
38
+ 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,101 @@
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, foreign_type: 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
+ foreign_type ||= :"#{name.to_s.underscore}_type" if resource.polymorphic?
75
+
76
+ opts = {readable: :gql?, only: [:readable], schema: false}
77
+ attribute name, :hash, opts do
78
+ prc = self.class.attribute_blocks[foreign_key]
79
+ fk = prc ? instance_eval(&prc) : @object.send(foreign_key)
80
+
81
+ typename = type
82
+ if resource.polymorphic?
83
+ prc = self.class.attribute_blocks[foreign_type]
84
+ ft = prc ? instance_eval(&prc) : @object.send(foreign_type)
85
+ typename = type[ft]
86
+ end
87
+
88
+ if fk && typename.present?
89
+ {__typename: typename, id: fk.to_s}
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Certain attributes should only work in GQL context
96
+ def gql?
97
+ Graphiti.context[:graphql]
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,190 @@
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
+ elsif federated_resource.polymorphic?
70
+ add_federated_resource_interface(federated_resource)
71
+ else
72
+ add_federated_resource_type(federated_resource.klass_name)
73
+ end
74
+
75
+ yield type_class, federated_resource
76
+ end
77
+ end
78
+
79
+ def add_federated_resource_interface(federated_resource)
80
+ interface = define_polymorphic_federated_resource_interface(federated_resource.klass_name)
81
+ federated_resource.type_name.values.each do |name|
82
+ add_federated_resource_type(name, interface: interface)
83
+ end
84
+ interface
85
+ end
86
+
87
+ def define_polymorphic_federated_resource_interface(klass_name)
88
+ interface = Module.new
89
+ interface.send(:include, @schema.class.base_interface)
90
+ interface.graphql_name(klass_name)
91
+ interface.field :id, String, null: false, external: true
92
+ type_registry[klass_name] = {type: interface, interface: true}
93
+ interface
94
+ end
95
+
96
+ def add_federated_resource_type(klass_name, interface: nil)
97
+ federated_type = Class.new(@schema.class.base_object)
98
+ federated_type.graphql_name klass_name
99
+ federated_type.key(fields: "id")
100
+ federated_type.extend_type
101
+ federated_type.implements(interface) if interface
102
+ federated_type.field :id, String, null: false, external: true
103
+ federated_type.class_eval do
104
+ def self.resolve_reference(reference, _context, _lookup)
105
+ reference
106
+ end
107
+ end
108
+ # NB must be registered before processing relationships
109
+ type_registry[klass_name] = {type: federated_type}
110
+ federated_type
111
+ end
112
+
113
+ def define_connection_type(name, type_class)
114
+ name = "#{name}FederatedConnection"
115
+ if (registered = type_registry[name])
116
+ return registered[:type]
117
+ end
118
+
119
+ klass = Class.new(@schema.class.base_object)
120
+ klass.graphql_name(name)
121
+ klass.field :nodes,
122
+ [type_class],
123
+ null: false,
124
+ extras: [:lookahead]
125
+ @schema.send :register, name, klass
126
+ klass
127
+ end
128
+
129
+ def define_federated_has_many(type_class, relationship)
130
+ local_name = GraphitiGraphQL::GraphitiSchema::Resource
131
+ .gql_name(relationship.local_resource_class.name)
132
+ local_type = type_registry[local_name][:type]
133
+ local_resource_name = type_registry[local_name][:resource]
134
+ local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
135
+
136
+ local_interface = type_registry["I#{local_name}"]
137
+ best_type = local_interface ? local_interface[:type] : local_type
138
+
139
+ connection_type = define_connection_type(local_name, best_type)
140
+
141
+ field = type_class.field relationship.name,
142
+ connection_type,
143
+ null: false,
144
+ connection: false
145
+ @schema.send :define_arguments_for_sideload_field,
146
+ field, @schema.graphiti_schema.get_resource(local_resource_name)
147
+
148
+ type_class.define_method relationship.name do |**arguments|
149
+ {data: object, arguments: arguments}
150
+ end
151
+ connection_type.define_method :nodes do |lookahead:, **arguments|
152
+ params = object[:arguments].as_json
153
+ .deep_transform_keys { |key| key.to_s.underscore.to_sym }
154
+ selections = lookahead.selections.map(&:name)
155
+ selections << relationship.foreign_key
156
+ selections << :_type # polymorphism
157
+ params[:fields] = {local_resource.type => selections.join(",")}
158
+
159
+ if (sort = Util.parse_sort(params[:sort]))
160
+ params[:sort] = sort
161
+ end
162
+
163
+ Federation::Loaders::HasMany
164
+ .for(relationship, params)
165
+ .load(object[:data][:id])
166
+ end
167
+ end
168
+
169
+ def define_federated_belongs_to(federated_resource, relationship)
170
+ type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
171
+ local_type = type_registry[type_name][:type]
172
+
173
+ # Todo maybe better way here
174
+ interface = type_registry["I#{type_name}"]
175
+
176
+ local_type = interface[:type] if interface
177
+ local_types = [local_type]
178
+ if interface
179
+ local_types |= interface[:implementers]
180
+ end
181
+
182
+ local_types.each do |local|
183
+ local.field relationship.name,
184
+ type_registry[federated_resource.klass_name][:type],
185
+ null: true
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end