Skip to content

Commit f7861e5

Browse files
authored
v1.5.1 (#159)
1 parent 7b84cdf commit f7861e5

29 files changed

+270
-184
lines changed

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ GraphQL stitching composes a single schema from multiple underlying GraphQL reso
1010
- Shared objects, fields, enums, and inputs across locations.
1111
- Multiple and composite type keys.
1212
- Combining local and remote schemas.
13-
- File uploads via [multipart form spec](https://github.com/jaydenseric/graphql-multipart-request-spec).
13+
- [File uploads](./docs/http_executable.md) via multipart forms.
1414
- Tested with all minor versions of `graphql-ruby`.
1515

1616
**NOT Supported:**
@@ -35,7 +35,7 @@ require "graphql/stitching"
3535

3636
## Usage
3737

38-
The quickest way to start is to use the provided [`Client`](./docs/client.md) component that wraps a stitched graph in an executable workflow (with optional query plan caching hooks):
38+
The [`Client`](./docs/client.md) component builds a stitched graph wrapped in an executable workflow (with optional query plan caching hooks):
3939

4040
```ruby
4141
movies_schema = <<~GRAPHQL
@@ -75,7 +75,7 @@ result = client.execute(
7575

7676
Schemas provided in [location settings](./docs/composer.md#performing-composition) may be class-based schemas with local resolvers (locally-executable schemas), or schemas built from SDL strings (schema definition language parsed using `GraphQL::Schema.from_definition`) and mapped to remote locations via [executables](#executables).
7777

78-
While the `Client` constructor is an easy quick start, the library also has several discrete components that can be assembled into custom workflows:
78+
While `Client` is sufficient for most usecases, the library offers several discrete components that can be assembled into tailored workflows:
7979

8080
- [Composer](./docs/composer.md) - merges and validates many schemas into one supergraph.
8181
- [Supergraph](./docs/supergraph.md) - manages the combined schema, location routing maps, and executable resources. Can be exported, cached, and rehydrated.
@@ -88,7 +88,7 @@ While the `Client` constructor is an easy quick start, the library also has seve
8888

8989
![Merging types](./docs/images/merging.png)
9090

91-
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
91+
To facilitate this merging of types, stitching must know how to cross-reference and fetch each variant of a type from its source location using [type resolver queries](#merged-type-resolver-queries). For those in an Apollo ecosystem, there's also _limited_ support for merging types though a [federation `_entities` protocol](./docs/federation_entities.md).
9292

9393
### Merged type resolver queries
9494

@@ -249,7 +249,7 @@ type Query {
249249
}
250250
```
251251

252-
See [resolver arguments](./docs/resolver.md#arguments) for full documentation on shaping input.
252+
See [resolver arguments](./docs/type_resolver.md#arguments) for full documentation on shaping input.
253253

254254
#### Composite type keys
255255

@@ -458,6 +458,7 @@ This repo includes working examples of stitched schemas running across small Rac
458458

459459
- [Merged types](./examples/merged_types)
460460
- [File uploads](./examples/file_uploads)
461+
- [Subscriptions](./examples/subscriptions)
461462

462463
## Tests
463464

docs/client.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ client.on_cache_write do |request, payload|
6868
end
6969
```
7070

71+
All request digests use SHA2 by default. You can swap in [a faster algorithm](https://github.com/Shopify/blake3-rb) and/or add base scoping by reconfiguring the stitching library:
72+
73+
```ruby
74+
GraphQL::Stitching.digest { |str| Digest::MD5.hexdigest("v2/#{str}") }
75+
```
76+
7177
Note that inlined input data works against caching, so you should _avoid_ these input literals when possible:
7278

7379
```graphql

docs/resolver.md renamed to docs/type_resolver.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
## GraphQL::Stitching::Resolver
1+
## GraphQL::Stitching::TypeResolver
22

3-
A `Resolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
3+
A `TypeResolver` contains all information about a root query used by stitching to fetch location-specific variants of a merged type. Specifically, resolvers manage parsed keys and argument structures.
44

55
### Arguments
66

7-
Resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
7+
Type resolvers configure arguments through a template string of [GraphQL argument literal syntax](https://spec.graphql.org/October2021/#sec-Language.Arguments). This allows sending multiple arguments that intermix stitching keys with complex object shapes and other static values.
88

99
#### Key insertions
1010

lib/graphql/stitching.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,24 @@ class CompositionError < StitchingError; end
2727
class ValidationError < CompositionError; end
2828

2929
class << self
30+
attr_writer :stitch_directive
31+
32+
# Proc used to compute digests; uses SHA2 by default.
33+
# @returns [Proc] proc used to compute digests.
34+
def digest(&block)
35+
if block_given?
36+
@digest = block
37+
else
38+
@digest ||= ->(str) { Digest::SHA2.hexdigest(str) }
39+
end
40+
end
41+
42+
# Name of the directive used to mark type resolvers.
43+
# @returns [String] name of the type resolver directive.
3044
def stitch_directive
3145
@stitch_directive ||= "stitch"
3246
end
3347

34-
attr_writer :stitch_directive
35-
3648
# Names of stitching directives to omit from the composed supergraph.
3749
# @returns [Array<String>] list of stitching directive names.
3850
def stitching_directive_names
@@ -50,6 +62,6 @@ def stitching_directive_names
5062
require_relative "stitching/plan"
5163
require_relative "stitching/planner"
5264
require_relative "stitching/request"
53-
require_relative "stitching/resolver"
65+
require_relative "stitching/type_resolver"
5466
require_relative "stitching/util"
5567
require_relative "stitching/version"

lib/graphql/stitching/composer.rb

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
require_relative "composer/base_validator"
44
require_relative "composer/validate_interfaces"
5-
require_relative "composer/validate_resolvers"
6-
require_relative "composer/resolver_config"
5+
require_relative "composer/validate_type_resolvers"
6+
require_relative "composer/type_resolver_config"
77

88
module GraphQL
99
module Stitching
@@ -31,7 +31,7 @@ class T < GraphQL::Schema::Object
3131
# @api private
3232
COMPOSITION_VALIDATORS = [
3333
ValidateInterfaces,
34-
ValidateResolvers,
34+
ValidateTypeResolvers,
3535
].freeze
3636

3737
# @return [String] name of the Query type in the composed schema.
@@ -168,7 +168,7 @@ def perform(locations_input)
168168
end
169169

170170
select_root_field_locations(schema)
171-
expand_abstract_resolvers(schema)
171+
expand_abstract_resolvers(schema, schemas)
172172

173173
supergraph = Supergraph.new(
174174
schema: schema,
@@ -199,8 +199,8 @@ def prepare_locations_input(locations_input)
199199
raise CompositionError, "The schema for `#{location}` location must be a GraphQL::Schema class."
200200
end
201201

202-
@resolver_configs.merge!(ResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
203-
@resolver_configs.merge!(ResolverConfig.extract_federation_entities(schema, location))
202+
@resolver_configs.merge!(TypeResolverConfig.extract_directive_assignments(schema, location, input[:stitch]))
203+
@resolver_configs.merge!(TypeResolverConfig.extract_federation_entities(schema, location))
204204

205205
schemas[location.to_s] = schema
206206
executables[location.to_s] = input[:executable] || schema
@@ -546,13 +546,13 @@ def extract_resolvers(type_name, types_by_location)
546546

547547
subgraph_field.directives.each do |directive|
548548
next unless directive.graphql_name == GraphQL::Stitching.stitch_directive
549-
resolver_configs << ResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
549+
resolver_configs << TypeResolverConfig.from_kwargs(directive.arguments.keyword_arguments)
550550
end
551551

552552
resolver_configs.each do |config|
553553
resolver_type_name = if config.type_name
554554
if !resolver_type.kind.abstract?
555-
raise CompositionError, "Resolver config may only specify a type name for abstract resolvers."
555+
raise CompositionError, "Type resolver config may only specify a type name for abstract resolvers."
556556
elsif !resolver_type.possible_types.find { _1.graphql_name == config.type_name }
557557
raise CompositionError, "Type `#{config.type_name}` is not a possible return type for query `#{field_name}`."
558558
end
@@ -561,7 +561,7 @@ def extract_resolvers(type_name, types_by_location)
561561
resolver_type.graphql_name
562562
end
563563

564-
key = Resolver.parse_key_with_types(
564+
key = TypeResolver.parse_key_with_types(
565565
config.key,
566566
@subgraph_types_by_name_and_location[resolver_type_name],
567567
)
@@ -581,11 +581,11 @@ def extract_resolvers(type_name, types_by_location)
581581
"#{argument.graphql_name}: $.#{key.primitive_name}"
582582
end
583583

584-
arguments = Resolver.parse_arguments_with_field(arguments_format, subgraph_field)
584+
arguments = TypeResolver.parse_arguments_with_field(arguments_format, subgraph_field)
585585
arguments.each { _1.verify_key(key) }
586586

587587
@resolver_map[resolver_type_name] ||= []
588-
@resolver_map[resolver_type_name] << Resolver.new(
588+
@resolver_map[resolver_type_name] << TypeResolver.new(
589589
location: location,
590590
type_name: resolver_type_name,
591591
field: subgraph_field.name,
@@ -620,15 +620,18 @@ def select_root_field_locations(schema)
620620

621621
# @!scope class
622622
# @!visibility private
623-
def expand_abstract_resolvers(schema)
623+
def expand_abstract_resolvers(composed_schema, schemas_by_location)
624624
@resolver_map.keys.each do |type_name|
625-
resolver_type = schema.types[type_name]
626-
next unless resolver_type.kind.abstract?
625+
next unless composed_schema.get_type(type_name).kind.abstract?
627626

628-
expanded_types = Util.expand_abstract_type(schema, resolver_type)
629-
expanded_types.select { @subgraph_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |expanded_type|
630-
@resolver_map[expanded_type.graphql_name] ||= []
631-
@resolver_map[expanded_type.graphql_name].push(*@resolver_map[type_name])
627+
@resolver_map[type_name].each do |resolver|
628+
abstract_type = @subgraph_types_by_name_and_location[type_name][resolver.location]
629+
expanded_types = Util.expand_abstract_type(schemas_by_location[resolver.location], abstract_type)
630+
631+
expanded_types.select { @subgraph_types_by_name_and_location[_1.graphql_name].length > 1 }.each do |impl_type|
632+
@resolver_map[impl_type.graphql_name] ||= []
633+
@resolver_map[impl_type.graphql_name].push(resolver)
634+
end
632635
end
633636
end
634637
end

lib/graphql/stitching/composer/resolver_config.rb renamed to lib/graphql/stitching/composer/type_resolver_config.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module GraphQL::Stitching
44
class Composer
5-
class ResolverConfig
5+
class TypeResolverConfig
66
ENTITY_TYPENAME = "_Entity"
77
ENTITIES_QUERY = "_entities"
88

@@ -30,7 +30,7 @@ def extract_federation_entities(schema, location)
3030
entity_type.directives.each do |directive|
3131
next unless directive.graphql_name == "key"
3232

33-
key = Resolver.parse_key(directive.arguments.keyword_arguments.fetch(:fields))
33+
key = TypeResolver.parse_key(directive.arguments.keyword_arguments.fetch(:fields))
3434
key_fields = key.map { "#{_1.name}: $.#{_1.name}" }
3535
field_path = "#{location}._entities"
3636

lib/graphql/stitching/composer/validate_resolvers.rb renamed to lib/graphql/stitching/composer/validate_type_resolvers.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module GraphQL::Stitching
44
class Composer
5-
class ValidateResolvers < BaseValidator
5+
class ValidateTypeResolvers < BaseValidator
66

77
def perform(supergraph, composer)
88
root_types = [

lib/graphql/stitching/executor.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# frozen_string_literal: true
22

33
require "json"
4-
require_relative "executor/resolver_source"
54
require_relative "executor/root_source"
5+
require_relative "executor/type_resolver_source"
66
require_relative "executor/shaper"
77

88
module GraphQL
@@ -66,7 +66,7 @@ def exec!(next_steps = [@after])
6666
.select { next_steps.include?(_1.after) }
6767
.group_by { [_1.location, _1.resolver.nil?] }
6868
.map do |(location, root_source), ops|
69-
source_class = root_source ? RootSource : ResolverSource
69+
source_class = root_source ? RootSource : TypeResolverSource
7070
@dataloader.with(source_class, self, location).request_all(ops)
7171
end
7272

lib/graphql/stitching/executor/shaper.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ def perform!(raw)
2323
def resolve_object_scope(raw_object, parent_type, selections, typename = nil)
2424
return nil if raw_object.nil?
2525

26-
typename ||= raw_object[Resolver::TYPENAME_EXPORT_NODE.alias]
27-
raw_object.reject! { |key, _v| Resolver.export_key?(key) }
26+
typename ||= raw_object[TypeResolver::TYPENAME_EXPORT_NODE.alias]
27+
raw_object.reject! { |key, _v| TypeResolver.export_key?(key) }
2828

2929
selections.each do |node|
3030
case node

lib/graphql/stitching/executor/resolver_source.rb renamed to lib/graphql/stitching/executor/type_resolver_source.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module GraphQL::Stitching
44
class Executor
5-
class ResolverSource < GraphQL::Dataloader::Source
5+
class TypeResolverSource < GraphQL::Dataloader::Source
66
def initialize(executor, location)
77
@executor = executor
88
@location = location
@@ -17,7 +17,7 @@ def fetch(ops)
1717

1818
if op.if_type
1919
# operations planned around unused fragment conditions should not trigger requests
20-
origin_set.select! { _1[Resolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
20+
origin_set.select! { _1[TypeResolver::TYPENAME_EXPORT_NODE.alias] == op.if_type }
2121
end
2222

2323
memo[op] = origin_set if origin_set.any?

0 commit comments

Comments
 (0)