graphql-stitching 0.0.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +27 -0
- data/.gitignore +59 -0
- data/.ruby-version +1 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +49 -0
- data/LICENSE +21 -0
- data/Procfile +3 -0
- data/README.md +329 -0
- data/Rakefile +12 -0
- data/docs/README.md +14 -0
- data/docs/composer.md +69 -0
- data/docs/document.md +15 -0
- data/docs/executor.md +29 -0
- data/docs/gateway.md +106 -0
- data/docs/images/library.png +0 -0
- data/docs/images/merging.png +0 -0
- data/docs/images/stitching.png +0 -0
- data/docs/planner.md +43 -0
- data/docs/shaper.md +20 -0
- data/docs/supergraph.md +65 -0
- data/example/gateway.rb +50 -0
- data/example/graphiql.html +153 -0
- data/example/remote1.rb +26 -0
- data/example/remote2.rb +26 -0
- data/graphql-stitching.gemspec +34 -0
- data/lib/graphql/stitching/composer/base_validator.rb +11 -0
- data/lib/graphql/stitching/composer/validate_boundaries.rb +80 -0
- data/lib/graphql/stitching/composer/validate_interfaces.rb +24 -0
- data/lib/graphql/stitching/composer.rb +442 -0
- data/lib/graphql/stitching/document.rb +59 -0
- data/lib/graphql/stitching/executor.rb +254 -0
- data/lib/graphql/stitching/gateway.rb +120 -0
- data/lib/graphql/stitching/planner.rb +323 -0
- data/lib/graphql/stitching/planner_operation.rb +59 -0
- data/lib/graphql/stitching/remote_client.rb +25 -0
- data/lib/graphql/stitching/shaper.rb +92 -0
- data/lib/graphql/stitching/supergraph.rb +171 -0
- data/lib/graphql/stitching/util.rb +63 -0
- data/lib/graphql/stitching/version.rb +7 -0
- data/lib/graphql/stitching.rb +30 -0
- metadata +142 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class PlannerOperation
|
6
|
+
attr_reader :key, :location, :parent_type, :type_condition, :operation_type, :insertion_path
|
7
|
+
attr_accessor :after_key, :selections, :variables, :boundary
|
8
|
+
|
9
|
+
def initialize(
|
10
|
+
key:,
|
11
|
+
location:,
|
12
|
+
parent_type:,
|
13
|
+
operation_type: "query",
|
14
|
+
insertion_path: [],
|
15
|
+
type_condition: nil,
|
16
|
+
after_key: nil,
|
17
|
+
selections: [],
|
18
|
+
variables: [],
|
19
|
+
boundary: nil
|
20
|
+
)
|
21
|
+
@key = key
|
22
|
+
@after_key = after_key
|
23
|
+
@location = location
|
24
|
+
@parent_type = parent_type
|
25
|
+
@operation_type = operation_type
|
26
|
+
@insertion_path = insertion_path
|
27
|
+
@type_condition = type_condition
|
28
|
+
@selections = selections
|
29
|
+
@variables = variables
|
30
|
+
@boundary = boundary
|
31
|
+
end
|
32
|
+
|
33
|
+
def selection_set
|
34
|
+
op = GraphQL::Language::Nodes::OperationDefinition.new(selections: @selections)
|
35
|
+
GraphQL::Language::Printer.new.print(op).gsub!(/\s+/, " ").strip!
|
36
|
+
end
|
37
|
+
|
38
|
+
def variable_set
|
39
|
+
@variables.each_with_object({}) do |(variable_name, value_type), memo|
|
40
|
+
memo[variable_name] = GraphQL::Language::Printer.new.print(value_type)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_h
|
45
|
+
{
|
46
|
+
"key" => @key,
|
47
|
+
"after_key" => @after_key,
|
48
|
+
"location" => @location,
|
49
|
+
"operation_type" => @operation_type,
|
50
|
+
"insertion_path" => @insertion_path,
|
51
|
+
"type_condition" => @type_condition,
|
52
|
+
"selections" => selection_set,
|
53
|
+
"variables" => variable_set,
|
54
|
+
"boundary" => @boundary,
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
require "json"
|
6
|
+
|
7
|
+
module GraphQL
|
8
|
+
module Stitching
|
9
|
+
class RemoteClient
|
10
|
+
def initialize(url:, headers:{})
|
11
|
+
@url = url
|
12
|
+
@headers = headers
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(location, document, variables)
|
16
|
+
response = Net::HTTP.post(
|
17
|
+
URI(@url),
|
18
|
+
{ "query" => document, "variables" => variables }.to_json,
|
19
|
+
{ "Content-Type" => "application/json" }.merge!(@headers)
|
20
|
+
)
|
21
|
+
JSON.parse(response.body)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# typed: false
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
module Stitching
|
6
|
+
class Shaper
|
7
|
+
def initialize(supergraph:, document:, raw:)
|
8
|
+
@supergraph = supergraph
|
9
|
+
@document = document
|
10
|
+
@result = raw
|
11
|
+
@errors = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform!
|
15
|
+
if @result.key?("data") && ! @result["data"].empty?
|
16
|
+
munge_entry(@result["data"], @document.operation.selections, @supergraph.schema.query)
|
17
|
+
# hate doing a second pass, but cannot remove _STITCH_ fields until the fragements are processed
|
18
|
+
clean_entry(@result["data"])
|
19
|
+
end
|
20
|
+
|
21
|
+
if @errors.length > 0
|
22
|
+
@result = [] unless @result.key?("errors")
|
23
|
+
@result["errors"] += @errors
|
24
|
+
end
|
25
|
+
|
26
|
+
@result
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def munge_entry(entry, selections, parent_type)
|
32
|
+
selections.each do |node|
|
33
|
+
case node
|
34
|
+
when GraphQL::Language::Nodes::Field
|
35
|
+
next if node.respond_to?(:name) && node&.name == "__typename"
|
36
|
+
|
37
|
+
munge_field(entry, node, parent_type)
|
38
|
+
|
39
|
+
when GraphQL::Language::Nodes::InlineFragment
|
40
|
+
next unless entry["_STITCH_typename"] == node.type.name
|
41
|
+
fragment_type = @supergraph.schema.types[node.type.name]
|
42
|
+
munge_entry(entry, node.selections, fragment_type)
|
43
|
+
|
44
|
+
when GraphQL::Language::Nodes::FragmentSpread
|
45
|
+
next unless entry["_STITCH_typename"] == node.name
|
46
|
+
fragment = @document.fragment_definitions[node.name]
|
47
|
+
fragment_type = @supergraph.schema.types[node.name]
|
48
|
+
munge_entry(entry, fragment.selections, fragment_type)
|
49
|
+
else
|
50
|
+
raise "Unexpected node of type #{node.class.name} in selection set."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def munge_field(entry, node, parent_type)
|
56
|
+
field_identifier = (node.alias || node.name)
|
57
|
+
node_type = Util.get_named_type_for_field_node(@supergraph.schema, parent_type, node)
|
58
|
+
|
59
|
+
if entry.nil?
|
60
|
+
return # TODO bubble up error if not nullable
|
61
|
+
end
|
62
|
+
|
63
|
+
child_entry = entry[field_identifier]
|
64
|
+
if child_entry.nil?
|
65
|
+
entry[field_identifier] = nil
|
66
|
+
elsif child_entry.is_a? Array
|
67
|
+
child_entry.each do |raw_item|
|
68
|
+
munge_entry(raw_item, node.selections, node_type)
|
69
|
+
end
|
70
|
+
elsif ! Util.is_leaf_type?(node_type)
|
71
|
+
munge_entry(child_entry, node.selections, node_type)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def clean_entry(entry)
|
76
|
+
return if entry.nil?
|
77
|
+
|
78
|
+
entry.each do |key, value|
|
79
|
+
if key.start_with? "_STITCH_"
|
80
|
+
entry.delete(key)
|
81
|
+
elsif value.is_a?(Array)
|
82
|
+
value.each do |item|
|
83
|
+
clean_entry(item)
|
84
|
+
end
|
85
|
+
elsif value.is_a?(Hash)
|
86
|
+
clean_entry(value)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Supergraph
|
6
|
+
LOCATION = "__super"
|
7
|
+
INTROSPECTION_TYPES = [
|
8
|
+
"__Schema",
|
9
|
+
"__Type",
|
10
|
+
"__Field",
|
11
|
+
"__Directive",
|
12
|
+
"__EnumValue",
|
13
|
+
"__InputValue",
|
14
|
+
"__TypeKind",
|
15
|
+
"__DirectiveLocation",
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
attr_reader :schema, :boundaries, :locations_by_type_and_field, :executables
|
19
|
+
|
20
|
+
def initialize(schema:, fields:, boundaries:, executables: {})
|
21
|
+
@schema = schema
|
22
|
+
@boundaries = boundaries
|
23
|
+
@locations_by_type_and_field = INTROSPECTION_TYPES.each_with_object(fields) do |type_name, memo|
|
24
|
+
introspection_type = schema.get_type(type_name)
|
25
|
+
next unless introspection_type.kind.fields?
|
26
|
+
|
27
|
+
memo[type_name] = introspection_type.fields.keys.each_with_object({}) do |field_name, m|
|
28
|
+
m[field_name] = [LOCATION]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
@possible_keys_by_type_and_location = {}
|
33
|
+
@executables = { LOCATION => @schema }.merge!(executables)
|
34
|
+
end
|
35
|
+
|
36
|
+
def fields
|
37
|
+
@locations_by_type_and_field.reject { |k, _v| INTROSPECTION_TYPES.include?(k) }
|
38
|
+
end
|
39
|
+
|
40
|
+
def export
|
41
|
+
return GraphQL::Schema::Printer.print_schema(@schema), {
|
42
|
+
"fields" => fields,
|
43
|
+
"boundaries" => @boundaries,
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.from_export(schema, delegation_map, executables: {})
|
48
|
+
schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
|
49
|
+
new(
|
50
|
+
schema: schema,
|
51
|
+
fields: delegation_map["fields"],
|
52
|
+
boundaries: delegation_map["boundaries"],
|
53
|
+
executables: executables,
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def assign_executable(location, executable = nil, &block)
|
58
|
+
executable ||= block
|
59
|
+
unless executable.is_a?(Class) && executable <= GraphQL::Schema
|
60
|
+
raise "A client or block handler must be provided." unless executable
|
61
|
+
raise "A client must be callable" unless executable.respond_to?(:call)
|
62
|
+
end
|
63
|
+
@executables[location] = executable
|
64
|
+
end
|
65
|
+
|
66
|
+
def execute_at_location(location, query, variables)
|
67
|
+
executable = executables[location]
|
68
|
+
|
69
|
+
if executable.nil?
|
70
|
+
raise "No executable assigned for #{location} location."
|
71
|
+
elsif executable.is_a?(Class) && executable <= GraphQL::Schema
|
72
|
+
executable.execute(
|
73
|
+
query: query,
|
74
|
+
variables: variables,
|
75
|
+
validate: false,
|
76
|
+
)
|
77
|
+
elsif executable.respond_to?(:call)
|
78
|
+
executable.call(location, query, variables)
|
79
|
+
else
|
80
|
+
raise "Missing valid executable for #{location} location."
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# inverts fields map to provide fields for a type/location
|
85
|
+
def fields_by_type_and_location
|
86
|
+
@fields_by_type_and_location ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
|
87
|
+
memo[type_name] = fields.each_with_object({}) do |(field_name, locations), memo|
|
88
|
+
locations.each do |location|
|
89
|
+
memo[location] ||= []
|
90
|
+
memo[location] << field_name
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def locations_by_type
|
97
|
+
@locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
|
98
|
+
memo[type_name] = fields.values.flatten.uniq
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def possible_keys_for_type_and_location(type_name, location)
|
103
|
+
possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
|
104
|
+
possible_keys_by_type[location] ||= begin
|
105
|
+
location_fields = fields_by_type_and_location[type_name][location] || []
|
106
|
+
location_fields & @boundaries[type_name].map { _1["selection"] }
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# For a given type, route from one origin service to one or more remote locations.
|
111
|
+
# Tunes a-star search to favor paths with fewest joining locations, ie:
|
112
|
+
# favor longer paths through target locations over shorter paths with additional locations.
|
113
|
+
def route_type_to_locations(type_name, start_location, goal_locations)
|
114
|
+
results = {}
|
115
|
+
costs = {}
|
116
|
+
|
117
|
+
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
118
|
+
[{ location: start_location, selection: possible_key, cost: 0 }]
|
119
|
+
end
|
120
|
+
|
121
|
+
while paths.any?
|
122
|
+
path = paths.pop
|
123
|
+
current_location = path.last[:location]
|
124
|
+
current_selection = path.last[:selection]
|
125
|
+
current_cost = path.last[:cost]
|
126
|
+
|
127
|
+
@boundaries[type_name].each do |boundary|
|
128
|
+
forward_location = boundary["location"]
|
129
|
+
next if current_selection != boundary["selection"]
|
130
|
+
next if path.any? { _1[:location] == forward_location }
|
131
|
+
|
132
|
+
best_cost = costs[forward_location] || Float::INFINITY
|
133
|
+
next if best_cost < current_cost
|
134
|
+
|
135
|
+
path.pop
|
136
|
+
path << {
|
137
|
+
location: current_location,
|
138
|
+
selection: current_selection,
|
139
|
+
cost: current_cost,
|
140
|
+
boundary: boundary,
|
141
|
+
}
|
142
|
+
|
143
|
+
if goal_locations.include?(forward_location)
|
144
|
+
current_result = results[forward_location]
|
145
|
+
if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
|
146
|
+
results[forward_location] = path.map { _1[:boundary] }
|
147
|
+
end
|
148
|
+
else
|
149
|
+
path.last[:cost] += 1
|
150
|
+
end
|
151
|
+
|
152
|
+
forward_cost = path.last[:cost]
|
153
|
+
costs[forward_location] = forward_cost if forward_cost < best_cost
|
154
|
+
|
155
|
+
possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
|
156
|
+
paths << [*path, { location: forward_location, selection: possible_key, cost: forward_cost }]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
paths.sort! do |a, b|
|
161
|
+
cost_diff = a.last[:cost] - b.last[:cost]
|
162
|
+
next cost_diff unless cost_diff.zero?
|
163
|
+
a.length - b.length
|
164
|
+
end.reverse!
|
165
|
+
end
|
166
|
+
|
167
|
+
results
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphQL
|
4
|
+
module Stitching
|
5
|
+
class Util
|
6
|
+
|
7
|
+
# gets the named type at the bottom of a non-null/list wrapper chain
|
8
|
+
def self.get_named_type(type)
|
9
|
+
while type.respond_to?(:of_type)
|
10
|
+
type = type.of_type
|
11
|
+
end
|
12
|
+
type
|
13
|
+
end
|
14
|
+
|
15
|
+
# gets a deep structural description of a list value type
|
16
|
+
def self.get_list_structure(type)
|
17
|
+
structure = []
|
18
|
+
previous = nil
|
19
|
+
while type.respond_to?(:of_type)
|
20
|
+
if type.is_a?(GraphQL::Schema::List)
|
21
|
+
structure.push(previous.is_a?(GraphQL::Schema::NonNull) ? "non_null_list" : "list")
|
22
|
+
end
|
23
|
+
if structure.any?
|
24
|
+
previous = type
|
25
|
+
if !type.of_type.respond_to?(:of_type)
|
26
|
+
structure.push(previous.is_a?(GraphQL::Schema::NonNull) ? "non_null_element" : "element")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
type = type.of_type
|
30
|
+
end
|
31
|
+
structure
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets all objects and interfaces that implement a given interface
|
35
|
+
def self.get_possible_types(schema, parent_type)
|
36
|
+
return [parent_type] unless parent_type.kind.abstract?
|
37
|
+
return parent_type.possible_types if parent_type.kind.union?
|
38
|
+
|
39
|
+
result = []
|
40
|
+
schema.types.values.each do |type|
|
41
|
+
next unless type <= GraphQL::Schema::Interface && type != parent_type
|
42
|
+
next unless type.interfaces.include?(parent_type)
|
43
|
+
result << type
|
44
|
+
result.push(*get_possible_types(schema, type)) if type.kind.interface?
|
45
|
+
end
|
46
|
+
result.uniq
|
47
|
+
end
|
48
|
+
|
49
|
+
# Specifies if a type is a leaf node (no children)
|
50
|
+
def self.is_leaf_type?(type)
|
51
|
+
type.kind.scalar? || type.kind.enum?
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.get_named_type_for_field_node(schema, parent_type, node)
|
55
|
+
if node.name == "__schema" && parent_type == schema.query
|
56
|
+
schema.types["__Schema"] # type mapped to phantom introspection field
|
57
|
+
else
|
58
|
+
Util.get_named_type(parent_type.fields[node.name].type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "graphql"
|
4
|
+
|
5
|
+
module GraphQL
|
6
|
+
module Stitching
|
7
|
+
class StitchingError < StandardError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
|
11
|
+
def stitch_directive
|
12
|
+
@stitch_directive ||= "stitch"
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_writer :stitch_directive
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
require_relative "stitching/gateway"
|
21
|
+
require_relative "stitching/supergraph"
|
22
|
+
require_relative "stitching/composer"
|
23
|
+
require_relative "stitching/document"
|
24
|
+
require_relative "stitching/executor"
|
25
|
+
require_relative "stitching/planner_operation"
|
26
|
+
require_relative "stitching/planner"
|
27
|
+
require_relative "stitching/remote_client"
|
28
|
+
require_relative "stitching/shaper"
|
29
|
+
require_relative "stitching/util"
|
30
|
+
require_relative "stitching/version"
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-stitching
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Greg MacWilliam
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-02-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphql
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.0.16
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.0.16
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '12.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '12.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.12'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.12'
|
69
|
+
description: GraphQL schema stitching for Ruby
|
70
|
+
email:
|
71
|
+
executables: []
|
72
|
+
extensions: []
|
73
|
+
extra_rdoc_files: []
|
74
|
+
files:
|
75
|
+
- ".github/workflows/ci.yml"
|
76
|
+
- ".gitignore"
|
77
|
+
- ".ruby-version"
|
78
|
+
- Gemfile
|
79
|
+
- Gemfile.lock
|
80
|
+
- LICENSE
|
81
|
+
- Procfile
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- docs/README.md
|
85
|
+
- docs/composer.md
|
86
|
+
- docs/document.md
|
87
|
+
- docs/executor.md
|
88
|
+
- docs/gateway.md
|
89
|
+
- docs/images/library.png
|
90
|
+
- docs/images/merging.png
|
91
|
+
- docs/images/stitching.png
|
92
|
+
- docs/planner.md
|
93
|
+
- docs/shaper.md
|
94
|
+
- docs/supergraph.md
|
95
|
+
- example/gateway.rb
|
96
|
+
- example/graphiql.html
|
97
|
+
- example/remote1.rb
|
98
|
+
- example/remote2.rb
|
99
|
+
- graphql-stitching.gemspec
|
100
|
+
- lib/graphql/stitching.rb
|
101
|
+
- lib/graphql/stitching/composer.rb
|
102
|
+
- lib/graphql/stitching/composer/base_validator.rb
|
103
|
+
- lib/graphql/stitching/composer/validate_boundaries.rb
|
104
|
+
- lib/graphql/stitching/composer/validate_interfaces.rb
|
105
|
+
- lib/graphql/stitching/document.rb
|
106
|
+
- lib/graphql/stitching/executor.rb
|
107
|
+
- lib/graphql/stitching/gateway.rb
|
108
|
+
- lib/graphql/stitching/planner.rb
|
109
|
+
- lib/graphql/stitching/planner_operation.rb
|
110
|
+
- lib/graphql/stitching/remote_client.rb
|
111
|
+
- lib/graphql/stitching/shaper.rb
|
112
|
+
- lib/graphql/stitching/supergraph.rb
|
113
|
+
- lib/graphql/stitching/util.rb
|
114
|
+
- lib/graphql/stitching/version.rb
|
115
|
+
homepage: https://github.com/gmac/graphql-stitching-ruby
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
metadata:
|
119
|
+
homepage_uri: https://github.com/gmac/graphql-stitching-ruby
|
120
|
+
changelog_uri: https://github.com/gmac/graphql-stitching-ruby/releases
|
121
|
+
source_code_uri: https://github.com/gmac/graphql-stitching-ruby
|
122
|
+
bug_tracker_uri: https://github.com/gmac/graphql-stitching-ruby/issues
|
123
|
+
post_install_message:
|
124
|
+
rdoc_options: []
|
125
|
+
require_paths:
|
126
|
+
- lib
|
127
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 3.1.1
|
132
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
|
+
requirements:
|
134
|
+
- - ">="
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubygems_version: 3.3.7
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: GraphQL schema stitching for Ruby
|
142
|
+
test_files: []
|