graphiti_graphql 0.1.1 → 0.1.2

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,26 @@
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
+ end
25
+ end
26
+ 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,89 @@
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)
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
+ opts = {readable: :gql?, only: [:readable], schema: false}
75
+ attribute name, :hash, opts do
76
+ prc = self.class.attribute_blocks[foreign_key]
77
+ fk = prc ? instance_eval(&prc) : @object.send(foreign_key)
78
+ {__typename: type, id: fk.to_s}
79
+ end
80
+ end
81
+ end
82
+
83
+ # Certain attributes should only work in GQL context
84
+ def gql?
85
+ Graphiti.context[:graphql]
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,150 @@
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
+ else
70
+ add_federated_resource_type(federated_resource.type_name)
71
+ end
72
+
73
+ yield type_class, federated_resource
74
+ end
75
+ end
76
+
77
+ def add_federated_resource_type(klass_name)
78
+ federated_type = Class.new(@schema.class.base_object)
79
+ federated_type.graphql_name klass_name
80
+ federated_type.key(fields: "id")
81
+ federated_type.extend_type
82
+ federated_type.field :id, String, null: false, external: true
83
+ federated_type.class_eval do
84
+ def self.resolve_reference(reference, _context, _lookup)
85
+ reference
86
+ end
87
+ end
88
+ # NB must be registered before processing relationships
89
+ type_registry[klass_name] = {type: federated_type}
90
+ federated_type
91
+ end
92
+
93
+ def define_federated_has_many(type_class, relationship)
94
+ local_name = GraphitiGraphQL::GraphitiSchema::Resource
95
+ .gql_name(relationship.local_resource_class.name)
96
+ local_type = type_registry[local_name][:type]
97
+ local_resource_name = type_registry[local_name][:resource]
98
+ local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
99
+
100
+ local_interface = type_registry["I#{local_name}"]
101
+ best_type = local_interface ? local_interface[:type] : local_type
102
+
103
+ field = type_class.field relationship.name,
104
+ [best_type],
105
+ null: false,
106
+ extras: [:lookahead]
107
+
108
+ @schema.send :define_arguments_for_sideload_field,
109
+ field, @schema.graphiti_schema.get_resource(local_resource_name)
110
+ type_class.define_method relationship.name do |lookahead:, **arguments|
111
+ # TODO test params...do version of sort with array/symbol keys and plain string
112
+ params = arguments.as_json
113
+ .deep_transform_keys { |key| key.to_s.underscore.to_sym }
114
+ selections = lookahead.selections.map(&:name)
115
+ selections << relationship.foreign_key
116
+ selections << :_type # polymorphism
117
+ params[:fields] = {local_resource.type => selections.join(",")}
118
+
119
+ if (sort = Util.parse_sort(params[:sort]))
120
+ params[:sort] = sort
121
+ end
122
+
123
+ Federation::Loaders::HasMany
124
+ .for(relationship, params)
125
+ .load(object[:id])
126
+ end
127
+ end
128
+
129
+ def define_federated_belongs_to(federated_resource, relationship)
130
+ type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
131
+ local_type = type_registry[type_name][:type]
132
+
133
+ # Todo maybe better way here
134
+ interface = type_registry["I#{type_name}"]
135
+
136
+ local_type = interface[:type] if interface
137
+ local_types = [local_type]
138
+ if interface
139
+ local_types |= interface[:implementers]
140
+ end
141
+
142
+ local_types.each do |local|
143
+ local.field relationship.name,
144
+ type_registry[federated_resource.type_name][:type],
145
+ null: true
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -59,14 +59,6 @@ module GraphitiGraphQL
59
59
  !!config[:remote]
60
60
  end
61
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
62
  def name
71
63
  config[:name]
72
64
  end
@@ -102,16 +94,6 @@ module GraphitiGraphQL
102
94
  def all_attributes
103
95
  attributes.merge(extra_attributes)
104
96
  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
97
  end
116
98
  end
117
99
  end
@@ -16,21 +16,6 @@ module GraphitiGraphQL
16
16
  def resources
17
17
  schema[:resources].map { |r| get_resource(r[:name]) }
18
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
19
  end
35
20
  end
36
21
  end