apollo-federation 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 446efb9b173791b74a0bfc8b6dff598f2540517b30fdcf3425a0b332303bf1f6
4
+ data.tar.gz: e7ddcd984c0ce2d9573b98459006577478bbdb5cc5347f6198eba17b5aae5494
5
+ SHA512:
6
+ metadata.gz: f1978c886ed68a721c277c6589c8890445480a85998120f7454b40f45144afebb63f55ba8425473e1d04a82c39e1005639b368c5cd90d0cf5d0138a1180fb0a2
7
+ data.tar.gz: 811f1c1eaf17c1c4a795b6bd1bce88a95cb2699d0e4d54e3dc152a04702f340e25b4af158de14f9d5e954d564a6693d048df7ff76ac93571abac8d00f894529e
@@ -0,0 +1,3 @@
1
+ ## 0.1.0 (Jun 21, 2019)
2
+
3
+ * First release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 Gusto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,152 @@
1
+ # apollo-federation
2
+
3
+ This gem extends the [GraphQL Ruby](http://graphql-ruby.org/) gem to add support for creating an [Apollo Federation](https://www.apollographql.com/docs/apollo-server/federation/introduction/) schema.
4
+
5
+ ## DISCLAIMER
6
+
7
+ This gem is still in a beta stage and may have some bugs or incompatibilities. See the [Known Issues and Limitations](#known-issues-and-limitations) below. If you run into any problems, please [file an issue](https://github.com/Gusto/apollo-federation-ruby/issues).
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'apollo-federation'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install apollo-federation
24
+
25
+ ## Getting Started
26
+
27
+ Include the `ApolloFederation::Field` module in your base field class:
28
+
29
+ ```ruby
30
+ require 'apollo-federation'
31
+
32
+ class BaseField < GraphQL::Schema::Field
33
+ include ApolloFederation::Field
34
+ end
35
+ ```
36
+
37
+ Include the `ApolloFederation::Object` module in your base object class:
38
+
39
+ ```ruby
40
+ class BaseObject < GraphQL::Schema::Object
41
+ include ApolloFederation::Object
42
+
43
+ field_class BaseField
44
+ end
45
+ ```
46
+
47
+ Finally, include the `ApolloFederation::Schema` module in your schema:
48
+
49
+ ```ruby
50
+ class MySchema < GraphQL::Schema
51
+ include ApolloFederation::Schema
52
+ end
53
+ ```
54
+
55
+ ## Example
56
+
57
+ The [`example`](./example/) folder contains a Ruby implementation of Apollo's [`federation-demo`](https://github.com/apollographql/federation-demo). To run it locally, install the Ruby dependencies:
58
+
59
+ $ bundle
60
+
61
+ Install the Node dependencies:
62
+
63
+ $ yarn
64
+
65
+ Start all of the services:
66
+
67
+ $ yarn start-services
68
+
69
+ Start the gateway:
70
+
71
+ $ yarn start-gateway
72
+
73
+ This will start up the gateway and serve it at http://localhost:5000.
74
+
75
+
76
+ ## Usage
77
+
78
+ The API is designed to mimic the API of [Apollo's federation library](https://www.apollographql.com/docs/apollo-server/federation/introduction/). It's best to read and understand the way federation works, in general, before attempting to use this library.
79
+
80
+ ### Extending a type
81
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#extending-external-types)
82
+
83
+ Call `extend_type` within your class definition:
84
+
85
+ ```ruby
86
+ class User < BaseObject
87
+ extend_type
88
+ end
89
+ ```
90
+
91
+ ### The `@key` directive
92
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#entities-and-keys)
93
+
94
+ Call `key` within your class definition:
95
+
96
+ ```ruby
97
+ class User < BaseObject
98
+ key fields: 'id'
99
+ end
100
+ ```
101
+
102
+ ### The `@external` directive
103
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/federation/core-concepts/#referencing-external-types)
104
+
105
+ Pass the `external: true` option to your field definition:
106
+
107
+ ```ruby
108
+ class User < BaseObject
109
+ field :id, ID, null: false, external: true
110
+ end
111
+ ```
112
+
113
+ ### The `@requires` directive
114
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/federation/advanced-features/#computed-fields)
115
+
116
+ Pass the `requires:` option to your field definition:
117
+
118
+ ```ruby
119
+ class Product < BaseObject
120
+ field :price, Int, null: true, external: true
121
+ field :weight, Int, null: true, external: true
122
+ field :shipping_estimate, Int, null: true, requires: { fields: "price weight"}
123
+ end
124
+ ```
125
+
126
+ ### The `@provides` directive
127
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/federation/advanced-features/#using-denormalized-data)
128
+
129
+ Pass the `provides:` option to your field definition:
130
+
131
+ ```ruby
132
+ class Review < BaseObject
133
+ field :author, 'User', null: true, provides: { fields: 'username' }
134
+ end
135
+ ```
136
+
137
+ ### Reference resolvers
138
+ [Apollo documentation](https://www.apollographql.com/docs/apollo-server/api/apollo-federation/#__resolvereference)
139
+
140
+ Define a `resolve_reference` class method on your object. The method will be passed the reference from another service and the context for the query.
141
+
142
+ ```ruby
143
+ class User < BaseObject
144
+ def self.resolve_reference(reference, context)
145
+ USERS.find { |user| user[:id] == reference[:id] }
146
+ end
147
+ end
148
+ ```
149
+
150
+ ## Known Limitations and Issues
151
+ - Currently only works with class-based schemas
152
+ - Does add directives to the output of `Schema.to_definition`. Since `graphql-ruby` doesn't natively support schema directives, the directives will only be visible to the [Apollo Gateway](https://www.apollographql.com/docs/apollo-server/api/apollo-gateway/) through the `Query._service` field (see the [Apollo Federation specification](https://www.apollographql.com/docs/apollo-server/federation/federation-spec/))
@@ -0,0 +1,4 @@
1
+ require 'apollo-federation/version'
2
+ require 'apollo-federation/schema'
3
+ require 'apollo-federation/object'
4
+ require 'apollo-federation/field'
@@ -0,0 +1,17 @@
1
+ require 'graphql'
2
+
3
+ module ApolloFederation
4
+ class Any < GraphQL::Schema::Scalar
5
+ graphql_name '_Any'
6
+
7
+ def self.coerce_input(value, _ctx)
8
+ # TODO: Should we convert it to a Mash-like object?
9
+ result = Hash.new
10
+ value.each_key do |key|
11
+ result[key.to_sym] = value[key]
12
+ end
13
+
14
+ result
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ require 'graphql'
2
+ require 'apollo-federation/any'
3
+
4
+ module ApolloFederation
5
+ module EntitiesField
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ end
9
+
10
+ module ClassMethods
11
+ extend GraphQL::Schema::Member::HasFields
12
+
13
+ def define_entities_field(entity_type)
14
+ field(:_entities, [entity_type, null: true], null: false) do
15
+ argument :representations, [Any], required: true
16
+ end
17
+ end
18
+ end
19
+
20
+ def _entities(representations:)
21
+ representations.map do |reference|
22
+ typename = reference[:__typename]
23
+ # TODO: Use warden or schema?
24
+ type = context.warden.get_type(typename)
25
+ if type.nil? || type.kind != GraphQL::TypeKinds::OBJECT
26
+ # TODO: Raise a specific error class?
27
+ raise "The _entities resolver tried to load an entity for type \"#{typename}\", but no object type of that name was found in the schema"
28
+ end
29
+
30
+ # TODO: Handle non-class types?
31
+ type_class = type.metadata[:type_class]
32
+ result = type_class.respond_to?(:resolve_reference) ?
33
+ type_class.resolve_reference(reference, context) :
34
+ reference
35
+
36
+ # TODO: This isn't 100% correct: if (for some reason) 2 different resolve_reference calls
37
+ # return the same object, it might not have the right type
38
+ # Right now, apollo-federation just adds a __typename property to the result,
39
+ # but I don't really like the idea of modifying the resolved object
40
+ context[result] = type
41
+ # TODO: Handle lazy objects?
42
+ result
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ require 'graphql'
2
+
3
+ module ApolloFederation
4
+ class Entity < GraphQL::Schema::Union
5
+ graphql_name '_Entity'
6
+
7
+ def self.resolve_type(object, context)
8
+ context[object]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ require 'graphql'
2
+ require 'apollo-federation/service'
3
+
4
+ module ApolloFederation
5
+ class FederatedDocumentFromSchemaDefinition < GraphQL::Language::DocumentFromSchemaDefinition
6
+ FEDERATION_TYPES = [
7
+ '_Any',
8
+ '_Entity',
9
+ '_Service',
10
+ ]
11
+ FEDERATION_QUERY_FIELDS = [
12
+ '_entities',
13
+ '_service',
14
+ ]
15
+
16
+ def build_object_type_node(object_type)
17
+ object_node = super
18
+ if query_type?(object_type)
19
+ federation_fields = object_node.fields.select { |field| FEDERATION_QUERY_FIELDS.include?(field.name) }
20
+ federation_fields.each { |field| object_node = object_node.delete_child(field) }
21
+ end
22
+ merge_directives(object_node, object_type.metadata[:federation_directives])
23
+ end
24
+
25
+ def build_field_node(field_type)
26
+ field_node = super
27
+ merge_directives(field_node, field_type.metadata[:federation_directives])
28
+ end
29
+
30
+ def build_type_definition_nodes(types)
31
+ non_federation_types = types.select do |type|
32
+ if query_type?(type)
33
+ !type.fields.values.all? { |field| FEDERATION_QUERY_FIELDS.include?(field.graphql_name) }
34
+ else
35
+ !FEDERATION_TYPES.include?(type.graphql_name)
36
+ end
37
+ end
38
+ super(non_federation_types)
39
+ end
40
+
41
+ private
42
+
43
+ def query_type?(type)
44
+ type == warden.root_type_for_operation('query')
45
+ end
46
+
47
+ def merge_directives(node, directives)
48
+ (directives || []).each do |directive|
49
+ node = node.merge_directive(
50
+ name: directive[:name],
51
+ arguments: build_arguments_node(directive[:arguments])
52
+ )
53
+ end
54
+ node
55
+ end
56
+
57
+ def build_arguments_node(arguments)
58
+ (arguments || []).map do |arg|
59
+ GraphQL::Language::Nodes::Argument.new(name: arg[:name], value: arg[:values])
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ require 'apollo-federation/has_directives'
2
+
3
+ module ApolloFederation
4
+ module Field
5
+ include HasDirectives
6
+
7
+ def initialize(*args, external: false, requires: nil, provides: nil, **kwargs, &block)
8
+ if external
9
+ add_directive(name: 'external')
10
+ end
11
+ if requires
12
+ add_directive(
13
+ name: 'requires',
14
+ arguments: [
15
+ name: 'fields',
16
+ values: requires[:fields],
17
+ ],
18
+ )
19
+ end
20
+ if provides
21
+ add_directive(
22
+ name: 'provides',
23
+ arguments: [
24
+ name: 'fields',
25
+ values: provides[:fields],
26
+ ],
27
+ )
28
+ end
29
+
30
+ # Pass on the default args:
31
+ super(*args, **kwargs, &block)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,15 @@
1
+
2
+ module ApolloFederation
3
+ module HasDirectives
4
+ def add_directive(name:, arguments: nil)
5
+ @federation_directives ||= []
6
+ @federation_directives << { name: name, arguments: arguments }
7
+ end
8
+
9
+ def to_graphql
10
+ field_defn = super # Returns a GraphQL::Field
11
+ field_defn.metadata[:federation_directives] = @federation_directives
12
+ field_defn
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ require 'apollo-federation/has_directives'
2
+
3
+ module ApolloFederation
4
+ module Object
5
+ def self.included(klass)
6
+ klass.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ include HasDirectives
11
+
12
+ # TODO: We should support extending interfaces at some point
13
+ def extend_type
14
+ add_directive(name: 'extends')
15
+ end
16
+
17
+ def key(fields:)
18
+ add_directive(
19
+ name: 'key',
20
+ arguments: [
21
+ name: 'fields',
22
+ values: fields,
23
+ ],
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ require 'apollo-federation/entities_field'
2
+ require 'apollo-federation/service_field'
3
+ require 'apollo-federation/entity'
4
+ require 'apollo-federation/federated_document_from_schema_definition.rb'
5
+
6
+ module ApolloFederation
7
+ module Schema
8
+ def self.included(klass)
9
+ klass.extend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ def to_graphql
14
+ orig_defn = super
15
+
16
+ if query.nil?
17
+ base = GraphQL::Schema::Object
18
+ else
19
+ base = query.metadata[:type_class]
20
+ end
21
+
22
+ federation_query = Class.new(base) do
23
+ graphql_name 'Query'
24
+
25
+ include EntitiesField
26
+ include ServiceField
27
+ end
28
+
29
+ possible_entities = orig_defn.types.values.select do |type|
30
+ !type.introspection? && !type.default_scalar? &&
31
+ type.metadata[:federation_directives]&.any? {|directive| directive[:name] == 'key'}
32
+ end
33
+
34
+ if possible_entities.length > 0
35
+ entity_type = Class.new(Entity) do
36
+ possible_types(*possible_entities)
37
+ end
38
+ # TODO: Should/can we encapsulate all of this inside the module? What's the best/most Ruby
39
+ # way to split this out?
40
+ federation_query.define_entities_field(entity_type)
41
+ end
42
+
43
+ query(federation_query)
44
+
45
+ super
46
+ end
47
+
48
+ def federation_sdl
49
+ @sdl ||= begin
50
+ document_from_schema = FederatedDocumentFromSchemaDefinition.new(self)
51
+ GraphQL::Language::Printer.new.print(document_from_schema.document)
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
@@ -0,0 +1,9 @@
1
+ require 'graphql'
2
+
3
+ module ApolloFederation
4
+ class Service < GraphQL::Schema::Object
5
+ graphql_name '_Service'
6
+
7
+ field(:sdl, String, null: true)
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ require 'graphql'
2
+ require 'apollo-federation/service'
3
+
4
+ module ApolloFederation
5
+ module ServiceField
6
+ extend GraphQL::Schema::Member::HasFields
7
+
8
+ field(:_service, Service, null: false)
9
+
10
+ def _service
11
+ { sdl: context.schema.class.federation_sdl }
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module ApolloFederation
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: apollo-federation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Noa Elad
8
+ - Rylan Collins
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2019-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: graphql
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: pry-byebug
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rack
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: rspec
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description: A Ruby implementation of Apollo Federation
71
+ email:
72
+ - noa.elad@gusto.com
73
+ - rylan@gusto.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - lib/apollo-federation.rb
82
+ - lib/apollo-federation/any.rb
83
+ - lib/apollo-federation/entities_field.rb
84
+ - lib/apollo-federation/entity.rb
85
+ - lib/apollo-federation/federated_document_from_schema_definition.rb
86
+ - lib/apollo-federation/field.rb
87
+ - lib/apollo-federation/has_directives.rb
88
+ - lib/apollo-federation/object.rb
89
+ - lib/apollo-federation/schema.rb
90
+ - lib/apollo-federation/service.rb
91
+ - lib/apollo-federation/service_field.rb
92
+ - lib/apollo-federation/version.rb
93
+ homepage: https://github.com/Gusto/apollo-federation-ruby
94
+ licenses:
95
+ - MIT
96
+ metadata:
97
+ homepage_uri: https://github.com/Gusto/apollo-federation-ruby
98
+ changelog_uri: https://github.com/Gusto/apollo-federation-ruby/releases
99
+ source_code_uri: https://github.com/Gusto/apollo-federation-ruby
100
+ bug_tracker_uri: https://github.com/Gusto/apollo-federation-ruby/issues
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.2.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.0.3
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: A Ruby implementation of Apollo Federation
120
+ test_files: []