graphql-stitching 1.6.2 → 1.7.1

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,152 @@
1
+ ## Serving a supergraph
2
+
3
+ Serving a stitched schema should be optimized by environment. In `production` we favor speed and stability over flexibility, while in `development` we favor the reverse. Among the simplest ways to deploy a stitched schema is to compose it locally, write the composed schema as a `.graphql` file in your repo, and then load the pre-composed schema into a stitching client at runtime. This assures that composition always happens before deployment where failures can be detected.
4
+
5
+ ### Exporting a production schema
6
+
7
+ 1. Make a helper class for building your supergraph and exporting it as an SDL string:
8
+
9
+ ```ruby
10
+ class SupergraphHelper
11
+ def self.export
12
+ client = GraphQL::Stitching::Client.new({
13
+ remote: {
14
+ schema: GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
15
+ },
16
+ local: {
17
+ schema: MyLocalSchema
18
+ }
19
+ })
20
+
21
+ client.supergraph.to_definition
22
+ end
23
+ end
24
+ ```
25
+
26
+ 2. Setup a `rake` task for writing the export to a repo file:
27
+
28
+ ```ruby
29
+ task :compose_supergraph do
30
+ File.write("db/schema/supergraph.graphql", SupergraphHelper.export)
31
+ puts "Schema composition was successful."
32
+ end
33
+
34
+ # bundle exec rake compose-supergraph
35
+ ```
36
+
37
+ 3. Also as part of the export Rake task, it's advisable to run a [schema comparator](https://github.com/xuorig/graphql-schema_comparator) across the `main` version and the current compilation to catch breaking change regressions that may arise [during composition](./composing_a_supergraph.md#schema-merge-patterns):
38
+
39
+ ```ruby
40
+ task :compose_supergraph do
41
+ # ...
42
+
43
+ supergraph_file = "db/schema/supergraph.graphql"
44
+ head_commit = %x(git merge-base HEAD origin/main).strip!
45
+ head_source = %x(git show #{head_commit}:#{supergraph_file})
46
+
47
+ old_schema = GraphQL::Schema.from_definition(head_source)
48
+ new_schema = GraphQL::Schema.from_definition(File.read(supergraph_file))
49
+ diff = GraphQL::SchemaComparator.compare(old_schema, new_schema)
50
+ raise "Breaking changes found:\n-#{diff.breaking_changes.join("\n-")}" if diff.breaking?
51
+
52
+ # ...
53
+ end
54
+ ```
55
+
56
+ 4. As a CI safeguard, be sure to write a test that compares the supergraph export against the current repo file. This assures the latest schema is always expored before deploying:
57
+
58
+ ```ruby
59
+ test "supergraph export is up to date." do
60
+ assert_equal SupergraphHelper.export, File.read("db/schema/supergraph.graphql")
61
+ end
62
+ ```
63
+
64
+ ### Supergraph controller
65
+
66
+ Then at runtime, execute requests using a client built for the environment. The `production` client should load the pre-composed export schema, while the `development` client can live reload using runtime composition. Be sure to memoize any static schemas that the development client uses to minimize reloading overhead:
67
+
68
+ ```ruby
69
+ class SupergraphController < ApplicationController
70
+ protect_from_forgery with: :null_session, prepend: true
71
+
72
+ def execute
73
+ # see visibility docs...
74
+ visibility_profile = select_visibility_profile_for_audience(current_user)
75
+
76
+ client.execute(
77
+ query: params[:query],
78
+ variables: params[:variables],
79
+ operation_name: params[:operation_name],
80
+ context: { visibility_profile: visibility_profile },
81
+ )
82
+ end
83
+
84
+ private
85
+
86
+ # select which client to use based on the environment...
87
+ def client
88
+ Rails.env.production? ? production_client : development_client
89
+ end
90
+
91
+ # production uses a pre-composed supergraph read from the repo...
92
+ def production_client
93
+ @production_client ||= begin
94
+ supergraph_sdl = File.read("db/schema/supergraph.graphql")
95
+
96
+ GraphQL::Stitching::Client.from_definition(supergraph_sdl, executables: {
97
+ remote: GraphQL::Stitching::HttpExecutable.new("https://api.remote.com/graphql"),
98
+ local: MyLocalSchema,
99
+ }).tap do |client|
100
+ # see performance and error handling docs...
101
+ client.on_cache_read { ... }
102
+ client.on_cache_write { ... }
103
+ client.on_error { ... }
104
+ end
105
+ end
106
+ end
107
+
108
+ # development uses a supergraph composed on the fly...
109
+ def development_client
110
+ GraphQL::Stitching::Client.new(locations: {
111
+ remote: {
112
+ schema: remote_schema,
113
+ executable: GraphQL::Stitching::HttpExecutable.new("https://localhost:3001/graphql"),
114
+ },
115
+ local: {
116
+ schema: MyLocalSchema,
117
+ },
118
+ })
119
+ end
120
+
121
+ # other flat schemas used in development should be
122
+ # cached in memory to avoid as much runtime overhead as possible
123
+ def remote_schema
124
+ @remote_schema ||= GraphQL::Schema.from_definition(File.read("db/schema/remote.graphql"))
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Client execution
130
+
131
+ The `Client.execute` method provides a mostly drop-in replacement for [`GraphQL::Schema.execute`](https://graphql-ruby.org/queries/executing_queries):
132
+
133
+ ```ruby
134
+ client.execute(
135
+ query: params[:query],
136
+ variables: params[:variables],
137
+ operation_name: params[:operation_name],
138
+ context: { visibility_profile: visibility_profile },
139
+ )
140
+ ```
141
+
142
+ It provides a subset of the standard `execute` arguments:
143
+
144
+ * `query`: a query (or mutation) as a string or parsed AST.
145
+ * `variables`: a hash of variables for the request.
146
+ * `operation_name`: the name of the operation to execute (when multiple are provided).
147
+ * `validate`: true if static validation should run on the supergraph schema before execution.
148
+ * `context`: an object passed through to executable calls and client hooks.
149
+
150
+ ### Production reloading
151
+
152
+ It is possible to "hot" reload a production supergraph (ie: update the graph without a server deployment) using a background process to poll a remote supergraph file for changes and then build it into a new client for the controller at runtime. This works fine as long as locations and their executables don't change. If locations will change, the runtime _must_ be prepared to dynamically generate appropraite location executables.
@@ -1,4 +1,4 @@
1
- ## Stitching subscriptions
1
+ ## Subscriptions
2
2
 
3
3
  Stitching is an interesting prospect for subscriptions because socket-based interactions can be isolated to their own schema/server with very little implementation beyond resolving entity keys. Then, entity data can be stitched onto subscription payloads from other locations.
4
4
 
@@ -0,0 +1,178 @@
1
+ # Visibility
2
+
3
+ Visibility controls can hide parts of a supergraph from select audiences without compromising stitching operations. Restricted schema elements are hidden from introspection and validate as though they do not exist (which is different from traditional authorization where an element is acknowledged as restricted). Visibility is useful for managing multiple distributions of a schema for different audiences, and provides a flexible analog to Apollo Federation's `@inaccessible` rule.
4
+
5
+ Under the hood, this system wraps [GraphQL visibility](https://graphql-ruby.org/authorization/visibility) (specifically, the newer `GraphQL::Schema::Visibility` with nil profile support) and requires at least GraphQL Ruby v2.5.3.
6
+
7
+ ## Example
8
+
9
+ Schemas may include a `@visibility` directive that defines element _profiles_. A profile is just a label describing an API distribution (public, private, etc). When a request is assigned a visibility profile, it can only access elements belonging to that profile. Elements without an explicit `@visibility` constraint belong to all profiles. For example:
10
+
11
+ _schemas/product_info.graphql_
12
+ ```graphql
13
+ directive @stitch(key: String!) on FIELD_DEFINITION
14
+ directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
15
+
16
+ type Product {
17
+ id: ID!
18
+ title: String!
19
+ description: String!
20
+ }
21
+
22
+ type Query {
23
+ featuredProduct: Product
24
+ product(id: ID!): Product @stitch(key: "id") @visibility(profiles: ["private"])
25
+ }
26
+ ```
27
+
28
+ _schemas/product_prices.graphql_
29
+ ```graphql
30
+ directive @stitch(key: String!) on FIELD_DEFINITION
31
+ directive @visibility(profiles: [String!]!) on OBJECT | INTERFACE | UNION | INPUT_OBJECT | ENUM | SCALAR | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE
32
+
33
+ type Product {
34
+ id: ID! @visibility(profiles: [])
35
+ msrp: Float! @visibility(profiles: ["private"])
36
+ price: Float!
37
+ }
38
+
39
+ type Query {
40
+ products(ids: [ID!]!): [Product]! @stitch(key: "id") @visibility(profiles: ["private"])
41
+ }
42
+ ```
43
+
44
+ When composing a stitching client, the names of all possible visibility profiles that the supergraph should respond to are specified in composer options:
45
+
46
+ ```ruby
47
+ client = GraphQL::Stitching::Client.new(
48
+ composer_options: {
49
+ visibility_profiles: ["public", "private"],
50
+ },
51
+ locations: {
52
+ info: {
53
+ schema: GraphQL::Schema.from_definition(File.read("schemas/product_info.graphql")),
54
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3001"),
55
+ },
56
+ prices: {
57
+ schema: GraphQL::Schema.from_definition(File.read("schemas/product_prices.graphql")),
58
+ executable: GraphQL::Stitching::HttpExecutable.new(url: "http://localhost:3002"),
59
+ },
60
+ }
61
+ )
62
+ ```
63
+
64
+ The client can then execute requests with a `visibility_profile` parameter in context that specifies one of these names:
65
+
66
+ ```ruby
67
+ query = %|{
68
+ featuredProduct {
69
+ title # always visible
70
+ price # always visible
71
+ msrp # only visible to "private" or without profile
72
+ id # only visible without profile
73
+ }
74
+ }|
75
+
76
+ result = client.execute(query, context: {
77
+ visibility_profile: "public", # << or "private"
78
+ })
79
+ ```
80
+
81
+ The `visibility_profile` parameter will select which visibility distribution to use while introspecting and validating the request. For example:
82
+
83
+ - Using `visibility_profile: "public"` will say the `msrp` field does not exist (because it is restricted to "private").
84
+ - Using `visibility_profile: "private"` will accesses the `msrp` field as usual.
85
+ - Providing no profile parameter (or `visibility_profile: nil`) will access the entire graph without any visibility constraints.
86
+
87
+ The full potential of visibility comes when hiding stitching implementation details, such as the `id` field (which is the stitching key for the Product type). While the `id` field is hidden from all named profiles, it remains operational for use by the stitching implementation.
88
+
89
+ ## Adding visibility directives
90
+
91
+ Add the `@visibility` directive into schemas using the library definition:
92
+
93
+ ```ruby
94
+ class QueryType < GraphQL::Schema::Object
95
+ field :my_field, String, null: true do |f|
96
+ f.directive(GraphQL::Stitching::Directives::Visibility, profiles: ["private"])
97
+ end
98
+ end
99
+
100
+ class MySchema < GraphQL::Schema
101
+ directive(GraphQL::Stitching::Directives::Visibility)
102
+ query(QueryType)
103
+ end
104
+ ```
105
+
106
+ ## Merging visibilities
107
+
108
+ Visibility directives merge across schemas into the narrowest constraint possible. Profiles for an element will intersect into its merged supergraph constraint:
109
+
110
+ ```graphql
111
+ # location 1
112
+ myField: String @visibility(profiles: ["a", "c"])
113
+
114
+ # location 2
115
+ myField: String @visibility(profiles: ["b", "c"])
116
+
117
+ # merged supergraph
118
+ myField: String @visibility(profiles: ["c"])
119
+ ```
120
+
121
+ This may cause an element's profiles to intersect into an empty set, which means the element belongs to no profiles and will be hidden from all named distributions:
122
+
123
+ ```graphql
124
+ # location 1
125
+ myField: String @visibility(profiles: ["a"])
126
+
127
+ # location 2
128
+ myField: String @visibility(profiles: ["b"])
129
+
130
+ # merged supergraph
131
+ myField: String @visibility(profiles: [])
132
+ ```
133
+
134
+ Locations may omit visibility information to give other locations full control. Remember that elements without a `@visibility` constraint belong to all profiles, which also applies while merging:
135
+
136
+ ```graphql
137
+ # location 1
138
+ myField: String
139
+
140
+ # location 2
141
+ myField: String @visibility(profiles: ["b"])
142
+
143
+ # merged supergraph
144
+ myField: String @visibility(profiles: ["b"])
145
+ ```
146
+
147
+ ## Type controls
148
+
149
+ Visibility controls can be applied to almost all GraphQL schema elements, including:
150
+
151
+ - Types (Object, Interface, Union, Enum, Scalar, InputObject)
152
+ - Fields (of Object and Interface)
153
+ - Arguments (of Field and InputObject)
154
+ - Enum values
155
+
156
+ While the visibility of type members (fields, arguments, and enum values) are pretty intuitive, the visibility of parent types is far more nuanced as constraints start to cascade:
157
+
158
+ ```graphql
159
+ type Widget @visibility(profiles: ["private"]) {
160
+ title: String
161
+ }
162
+
163
+ type Query {
164
+ widget: Widget # << GETS HIDDEN
165
+ }
166
+ ```
167
+
168
+ In this example, hiding the `Widget` type will also hide the `Query.widget` field that returns it. You can review materialized visibility profiles by printing their respective schemas:
169
+
170
+ ```ruby
171
+ public_schema = client.supergraph.to_definition(visibility_profile: "public")
172
+ File.write("schemas/supergraph_public.graphql", public_schema)
173
+
174
+ private_schema = client.supergraph.to_definition(visibility_profile: "private")
175
+ File.write("schemas/supergraph_private.graphql", private_schema)
176
+ ```
177
+
178
+ It's helpful to commit these outputs to your repo where you can monitor their diffs during the PR process.
@@ -7,22 +7,30 @@ module GraphQL
7
7
  # Client is an out-of-the-box helper that assembles all
8
8
  # stitching components into a workflow that executes requests.
9
9
  class Client
10
+ class << self
11
+ def from_definition(schema, executables:)
12
+ new(supergraph: Supergraph.from_definition(schema, executables: executables))
13
+ end
14
+ end
15
+
10
16
  # @return [Supergraph] composed supergraph that services incoming requests.
11
17
  attr_reader :supergraph
12
18
 
13
19
  # Builds a new client instance. Either `supergraph` or `locations` configuration is required.
14
20
  # @param supergraph [Supergraph] optional, a pre-composed supergraph that bypasses composer setup.
15
21
  # @param locations [Hash<Symbol, Hash<Symbol, untyped>>] optional, composer configurations for each graph location.
16
- # @param composer [Composer] optional, a pre-configured composer instance for use with `locations` configuration.
17
- def initialize(locations: nil, supergraph: nil, composer: nil)
22
+ # @param composer_options [Hash] optional, composer options for configuring composition.
23
+ def initialize(locations: nil, supergraph: nil, composer_options: {})
18
24
  @supergraph = if locations && supergraph
19
25
  raise ArgumentError, "Cannot provide both locations and a supergraph."
20
26
  elsif supergraph && !supergraph.is_a?(Supergraph)
21
27
  raise ArgumentError, "Provided supergraph must be a GraphQL::Stitching::Supergraph instance."
28
+ elsif supergraph && composer_options.any?
29
+ raise ArgumentError, "Cannot provide composer options with a pre-built supergraph."
22
30
  elsif supergraph
23
31
  supergraph
24
32
  else
25
- composer ||= Composer.new
33
+ composer = Composer.new(**composer_options)
26
34
  composer.perform(locations)
27
35
  end
28
36
 
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "composer/base_validator"
4
- require_relative "composer/supergraph_directives"
5
4
  require_relative "composer/validate_interfaces"
6
5
  require_relative "composer/validate_type_resolvers"
7
6
  require_relative "composer/type_resolver_config"
@@ -23,9 +22,9 @@ module GraphQL
23
22
 
24
23
  # @api private
25
24
  BASIC_VALUE_MERGER = ->(values_by_location, _info) { values_by_location.values.find { !_1.nil? } }
26
-
25
+
27
26
  # @api private
28
- BASIC_ROOT_FIELD_LOCATION_SELECTOR = ->(locations, _info) { locations.last }
27
+ VISIBILITY_PROFILES_MERGER = ->(values_by_location, _info) { values_by_location.values.reduce(:&) }
29
28
 
30
29
  # @api private
31
30
  COMPOSITION_VALIDATORS = [
@@ -52,11 +51,13 @@ module GraphQL
52
51
  query_name: "Query",
53
52
  mutation_name: "Mutation",
54
53
  subscription_name: "Subscription",
54
+ visibility_profiles: [],
55
55
  description_merger: nil,
56
56
  deprecation_merger: nil,
57
57
  default_value_merger: nil,
58
58
  directive_kwarg_merger: nil,
59
- root_field_location_selector: nil
59
+ root_field_location_selector: nil,
60
+ root_entrypoints: nil
60
61
  )
61
62
  @query_name = query_name
62
63
  @mutation_name = mutation_name
@@ -65,12 +66,14 @@ module GraphQL
65
66
  @deprecation_merger = deprecation_merger || BASIC_VALUE_MERGER
66
67
  @default_value_merger = default_value_merger || BASIC_VALUE_MERGER
67
68
  @directive_kwarg_merger = directive_kwarg_merger || BASIC_VALUE_MERGER
68
- @root_field_location_selector = root_field_location_selector || BASIC_ROOT_FIELD_LOCATION_SELECTOR
69
+ @root_field_location_selector = root_field_location_selector
70
+ @root_entrypoints = root_entrypoints || {}
69
71
 
70
72
  @field_map = {}
71
73
  @resolver_map = {}
72
74
  @resolver_configs = {}
73
75
  @mapped_type_names = {}
76
+ @visibility_profiles = Set.new(visibility_profiles)
74
77
  @subgraph_directives_by_name_and_location = nil
75
78
  @subgraph_types_by_name_and_location = nil
76
79
  @schema_directives = nil
@@ -85,9 +88,9 @@ module GraphQL
85
88
 
86
89
  directives_to_omit = [
87
90
  GraphQL::Stitching.stitch_directive,
88
- KeyDirective.graphql_name,
89
- ResolverDirective.graphql_name,
90
- SourceDirective.graphql_name,
91
+ Directives::SupergraphKey.graphql_name,
92
+ Directives::SupergraphResolver.graphql_name,
93
+ Directives::SupergraphSource.graphql_name,
91
94
  ]
92
95
 
93
96
  # "directive_name" => "location" => subgraph_directive
@@ -181,6 +184,10 @@ module GraphQL
181
184
  expand_abstract_resolvers(schema, schemas)
182
185
  apply_supergraph_directives(schema, @resolver_map, @field_map)
183
186
 
187
+ if (visibility_def = schema.directives[GraphQL::Stitching.visibility_directive])
188
+ visibility_def.get_argument("profiles").default_value(@visibility_profiles.to_a.sort)
189
+ end
190
+
184
191
  supergraph = Supergraph.from_definition(schema, executables: executables)
185
192
 
186
193
  COMPOSITION_VALIDATORS.each do |validator_class|
@@ -237,7 +244,7 @@ module GraphQL
237
244
 
238
245
  builder = self
239
246
 
240
- Class.new(GraphQL::Schema::Scalar) do
247
+ Class.new(GraphQL::Stitching::Supergraph::ScalarType) do
241
248
  graphql_name(type_name)
242
249
  description(builder.merge_descriptions(type_name, types_by_location))
243
250
  builder.build_merged_directives(type_name, types_by_location, self)
@@ -264,7 +271,7 @@ module GraphQL
264
271
  end
265
272
  end
266
273
 
267
- Class.new(GraphQL::Schema::Enum) do
274
+ Class.new(GraphQL::Stitching::Supergraph::EnumType) do
268
275
  graphql_name(type_name)
269
276
  description(builder.merge_descriptions(type_name, types_by_location))
270
277
  builder.build_merged_directives(type_name, types_by_location, self)
@@ -286,7 +293,7 @@ module GraphQL
286
293
  def build_object_type(type_name, types_by_location)
287
294
  builder = self
288
295
 
289
- Class.new(GraphQL::Schema::Object) do
296
+ Class.new(GraphQL::Stitching::Supergraph::ObjectType) do
290
297
  graphql_name(type_name)
291
298
  description(builder.merge_descriptions(type_name, types_by_location))
292
299
 
@@ -306,7 +313,7 @@ module GraphQL
306
313
  builder = self
307
314
 
308
315
  Module.new do
309
- include GraphQL::Schema::Interface
316
+ include GraphQL::Stitching::Supergraph::InterfaceType
310
317
  graphql_name(type_name)
311
318
  description(builder.merge_descriptions(type_name, types_by_location))
312
319
 
@@ -325,7 +332,7 @@ module GraphQL
325
332
  def build_union_type(type_name, types_by_location)
326
333
  builder = self
327
334
 
328
- Class.new(GraphQL::Schema::Union) do
335
+ Class.new(GraphQL::Stitching::Supergraph::UnionType) do
329
336
  graphql_name(type_name)
330
337
  description(builder.merge_descriptions(type_name, types_by_location))
331
338
 
@@ -340,7 +347,7 @@ module GraphQL
340
347
  def build_input_object_type(type_name, types_by_location)
341
348
  builder = self
342
349
 
343
- Class.new(GraphQL::Schema::InputObject) do
350
+ Class.new(GraphQL::Stitching::Supergraph::InputObjectType) do
344
351
  graphql_name(type_name)
345
352
  description(builder.merge_descriptions(type_name, types_by_location))
346
353
  builder.build_merged_arguments(type_name, types_by_location, self)
@@ -451,6 +458,7 @@ module GraphQL
451
458
  end
452
459
 
453
460
  directives_by_name_location.each do |directive_name, directives_by_location|
461
+ kwarg_merger = @directive_kwarg_merger
454
462
  directive_class = @schema_directives[directive_name]
455
463
  next unless directive_class
456
464
 
@@ -467,8 +475,20 @@ module GraphQL
467
475
  end
468
476
  end
469
477
 
478
+ if directive_class.graphql_name == GraphQL::Stitching.visibility_directive
479
+ unless GraphQL::Stitching.supports_visibility?
480
+ raise CompositionError, "Using `@#{GraphQL::Stitching.visibility_directive}` directive " \
481
+ "for schema visibility controls requires GraphQL Ruby v#{GraphQL::Stitching::MIN_VISIBILITY_VERSION} or later."
482
+ end
483
+
484
+ if (profiles = kwarg_values_by_name_location["profiles"])
485
+ @visibility_profiles.merge(profiles.each_value.reduce(&:|))
486
+ kwarg_merger = VISIBILITY_PROFILES_MERGER
487
+ end
488
+ end
489
+
470
490
  kwargs = kwarg_values_by_name_location.each_with_object({}) do |(kwarg_name, kwarg_values_by_location), memo|
471
- memo[kwarg_name.to_sym] = @directive_kwarg_merger.call(kwarg_values_by_location, {
491
+ memo[kwarg_name.to_sym] = kwarg_merger.call(kwarg_values_by_location, {
472
492
  type_name: type_name,
473
493
  field_name: field_name,
474
494
  argument_name: argument_name,
@@ -610,11 +630,20 @@ module GraphQL
610
630
  root_field_locations = @field_map[root_type.graphql_name][root_field_name]
611
631
  next unless root_field_locations.length > 1
612
632
 
613
- target_location = @root_field_location_selector.call(root_field_locations, {
614
- type_name: root_type.graphql_name,
615
- field_name: root_field_name,
616
- })
617
- next unless root_field_locations.include?(target_location)
633
+ root_field_path = "#{root_type.graphql_name}.#{root_field_name}"
634
+ target_location = if @root_field_location_selector && @root_entrypoints.empty?
635
+ Warning.warn("Composer option `root_field_location_selector` is deprecated and will be removed.")
636
+ @root_field_location_selector.call(root_field_locations, {
637
+ type_name: root_type.graphql_name,
638
+ field_name: root_field_name,
639
+ })
640
+ else
641
+ @root_entrypoints[root_field_path] || root_field_locations.last
642
+ end
643
+
644
+ unless root_field_locations.include?(target_location)
645
+ raise CompositionError, "Invalid `root_entrypoints` configuration: `#{root_field_path}` has no `#{target_location}` location."
646
+ end
618
647
 
619
648
  root_field_locations.reject! { _1 == target_location }
620
649
  root_field_locations.unshift(target_location)
@@ -693,8 +722,8 @@ module GraphQL
693
722
 
694
723
  keys_for_type.each do |key, locations|
695
724
  locations.each do |location|
696
- schema_directives[KeyDirective.graphql_name] ||= KeyDirective
697
- type.directive(KeyDirective, key: key, location: location)
725
+ schema_directives[Directives::SupergraphKey.graphql_name] ||= Directives::SupergraphKey
726
+ type.directive(Directives::SupergraphKey, key: key, location: location)
698
727
  end
699
728
  end
700
729
 
@@ -710,8 +739,8 @@ module GraphQL
710
739
  type_name: (resolver.type_name if resolver.type_name != type_name),
711
740
  }
712
741
 
713
- schema_directives[ResolverDirective.graphql_name] ||= ResolverDirective
714
- type.directive(ResolverDirective, **params.tap(&:compact!))
742
+ schema_directives[Directives::SupergraphResolver.graphql_name] ||= Directives::SupergraphResolver
743
+ type.directive(Directives::SupergraphResolver, **params.tap(&:compact!))
715
744
  end
716
745
  end
717
746
 
@@ -737,8 +766,8 @@ module GraphQL
737
766
 
738
767
  # Apply source directives to annotate the possible locations of each field
739
768
  locations_for_field.each do |location|
740
- schema_directives[SourceDirective.graphql_name] ||= SourceDirective
741
- field.directive(SourceDirective, location: location)
769
+ schema_directives[Directives::SupergraphSource.graphql_name] ||= Directives::SupergraphSource
770
+ field.directive(Directives::SupergraphSource, location: location)
742
771
  end
743
772
  end
744
773
  end
@@ -1,8 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GraphQL::Stitching
4
- class Composer
5
- class KeyDirective < GraphQL::Schema::Directive
4
+ module Directives
5
+ class Stitch < GraphQL::Schema::Directive
6
+ graphql_name "stitch"
7
+ locations FIELD_DEFINITION
8
+ argument :key, String, required: true
9
+ argument :arguments, String, required: false
10
+ argument :type_name, String, required: false
11
+ repeatable true
12
+ end
13
+
14
+ class Visibility < GraphQL::Schema::Directive
15
+ graphql_name "visibility"
16
+ locations(
17
+ OBJECT, INTERFACE, UNION, INPUT_OBJECT, ENUM, SCALAR,
18
+ FIELD_DEFINITION, ARGUMENT_DEFINITION, INPUT_FIELD_DEFINITION, ENUM_VALUE
19
+ )
20
+ argument :profiles, [String, null: false], required: true
21
+ end
22
+
23
+ class SupergraphKey < GraphQL::Schema::Directive
6
24
  graphql_name "key"
7
25
  locations OBJECT, INTERFACE, UNION
8
26
  argument :key, String, required: true
@@ -10,7 +28,7 @@ module GraphQL::Stitching
10
28
  repeatable true
11
29
  end
12
30
 
13
- class ResolverDirective < GraphQL::Schema::Directive
31
+ class SupergraphResolver < GraphQL::Schema::Directive
14
32
  graphql_name "resolver"
15
33
  locations OBJECT, INTERFACE, UNION
16
34
  argument :location, String, required: true
@@ -23,7 +41,7 @@ module GraphQL::Stitching
23
41
  repeatable true
24
42
  end
25
43
 
26
- class SourceDirective < GraphQL::Schema::Directive
44
+ class SupergraphSource < GraphQL::Schema::Directive
27
45
  graphql_name "source"
28
46
  locations FIELD_DEFINITION
29
47
  argument :location, String, required: true
@@ -57,6 +57,10 @@ module GraphQL
57
57
  @context[:request] = self
58
58
  end
59
59
 
60
+ def original_document
61
+ @query.document
62
+ end
63
+
60
64
  # @return [String] the original document string, or a print of the parsed AST document.
61
65
  def string
62
66
  with_prepared_document { @string || normalized_string }