apollo-federation 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,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: []