graphiti_graphql 0.1.0 → 0.1.5

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,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