Class: GraphQL::Stitching::Supergraph

Inherits:
Object
  • Object
show all
Defined in:
lib/graphql/stitching/supergraph.rb

Constant Summary collapse

LOCATION =
"__super"
INTROSPECTION_TYPES =
[
  "__Schema",
  "__Type",
  "__Field",
  "__Directive",
  "__EnumValue",
  "__InputValue",
  "__TypeKind",
  "__DirectiveLocation",
].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(schema:, fields:, boundaries:, executables: {}) ⇒ Supergraph

Returns a new instance of Supergraph.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/graphql/stitching/supergraph.rb', line 20

def initialize(schema:, fields:, boundaries:, executables: {})
  @schema = schema
  @boundaries = boundaries
  @locations_by_type_and_field = INTROSPECTION_TYPES.each_with_object(fields) do |type_name, memo|
    introspection_type = schema.get_type(type_name)
    next unless introspection_type.kind.fields?

    memo[type_name] = introspection_type.fields.keys.each_with_object({}) do |field_name, m|
      m[field_name] = [LOCATION]
    end
  end

  @possible_keys_by_type_and_location = {}
  @executables = { LOCATION => @schema }.merge!(executables)
end

Instance Attribute Details

#boundariesObject (readonly)

Returns the value of attribute boundaries.



18
19
20
# File 'lib/graphql/stitching/supergraph.rb', line 18

def boundaries
  @boundaries
end

#executablesObject (readonly)

Returns the value of attribute executables.



18
19
20
# File 'lib/graphql/stitching/supergraph.rb', line 18

def executables
  @executables
end

#locations_by_type_and_fieldObject (readonly)

Returns the value of attribute locations_by_type_and_field.



18
19
20
# File 'lib/graphql/stitching/supergraph.rb', line 18

def locations_by_type_and_field
  @locations_by_type_and_field
end

#schemaObject (readonly)

Returns the value of attribute schema.



18
19
20
# File 'lib/graphql/stitching/supergraph.rb', line 18

def schema
  @schema
end

Class Method Details

.from_export(schema, delegation_map, executables: {}) ⇒ Object



47
48
49
50
51
52
53
54
55
# File 'lib/graphql/stitching/supergraph.rb', line 47

def self.from_export(schema, delegation_map, executables: {})
  schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)
  new(
    schema: schema,
    fields: delegation_map["fields"],
    boundaries: delegation_map["boundaries"],
    executables: executables,
  )
end

Instance Method Details

#assign_executable(location, executable = nil, &block) ⇒ Object



57
58
59
60
61
62
63
64
# File 'lib/graphql/stitching/supergraph.rb', line 57

def assign_executable(location, executable = nil, &block)
  executable ||= block
  unless executable.is_a?(Class) && executable <= GraphQL::Schema
    raise StitchingError, "A client or block handler must be provided." unless executable
    raise StitchingError, "A client must be callable" unless executable.respond_to?(:call)
  end
  @executables[location] = executable
end

#execute_at_location(location, query, variables, context) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/graphql/stitching/supergraph.rb', line 66

def execute_at_location(location, query, variables, context)
  executable = executables[location]

  if executable.nil?
    raise StitchingError, "No executable assigned for #{location} location."
  elsif executable.is_a?(Class) && executable <= GraphQL::Schema
    executable.execute(
      query: query,
      variables: variables,
      context: context,
      validate: false,
    )
  elsif executable.respond_to?(:call)
    executable.call(location, query, variables, context)
  else
    raise StitchingError, "Missing valid executable for #{location} location."
  end
end

#exportObject



40
41
42
43
44
45
# File 'lib/graphql/stitching/supergraph.rb', line 40

def export
  return GraphQL::Schema::Printer.print_schema(@schema), {
    "fields" => fields,
    "boundaries" => @boundaries,
  }
end

#fieldsObject



36
37
38
# File 'lib/graphql/stitching/supergraph.rb', line 36

def fields
  @locations_by_type_and_field.reject { |k, _v| INTROSPECTION_TYPES.include?(k) }
end

#fields_by_type_and_locationObject

inverts fields map to provide fields for a type/location



86
87
88
89
90
91
92
93
94
95
# File 'lib/graphql/stitching/supergraph.rb', line 86

def fields_by_type_and_location
  @fields_by_type_and_location ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
    memo[type_name] = fields.each_with_object({}) do |(field_name, locations), memo|
      locations.each do |location|
        memo[location] ||= []
        memo[location] << field_name
      end
    end
  end
end

#locations_by_typeObject



97
98
99
100
101
# File 'lib/graphql/stitching/supergraph.rb', line 97

def locations_by_type
  @locations_by_type ||= @locations_by_type_and_field.each_with_object({}) do |(type_name, fields), memo|
    memo[type_name] = fields.values.flatten.uniq
  end
end

#possible_keys_for_type_and_location(type_name, location) ⇒ Object



103
104
105
106
107
108
109
# File 'lib/graphql/stitching/supergraph.rb', line 103

def possible_keys_for_type_and_location(type_name, location)
  possible_keys_by_type = @possible_keys_by_type_and_location[type_name] ||= {}
  possible_keys_by_type[location] ||= begin
    location_fields = fields_by_type_and_location[type_name][location] || []
    location_fields & @boundaries[type_name].map { _1["selection"] }
  end
end

#route_type_to_locations(type_name, start_location, goal_locations) ⇒ Object

For a given type, route from one origin service to one or more remote locations. Tunes a-star search to favor paths with fewest joining locations, ie: favor longer paths through target locations over shorter paths with additional locations.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/graphql/stitching/supergraph.rb', line 114

def route_type_to_locations(type_name, start_location, goal_locations)
  results = {}
  costs = {}

  paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
    [{ location: start_location, selection: possible_key, cost: 0 }]
  end

  while paths.any?
    path = paths.pop
    current_location = path.last[:location]
    current_selection = path.last[:selection]
    current_cost = path.last[:cost]

    @boundaries[type_name].each do |boundary|
      forward_location = boundary["location"]
      next if current_selection != boundary["selection"]
      next if path.any? { _1[:location] == forward_location }

      best_cost = costs[forward_location] || Float::INFINITY
      next if best_cost < current_cost

      path.pop
      path << {
        location: current_location,
        selection: current_selection,
        cost: current_cost,
        boundary: boundary,
      }

      if goal_locations.include?(forward_location)
        current_result = results[forward_location]
        if current_result.nil? || current_cost < best_cost || (current_cost == best_cost && path.length < current_result.length)
          results[forward_location] = path.map { _1[:boundary] }
        end
      else
        path.last[:cost] += 1
      end

      forward_cost = path.last[:cost]
      costs[forward_location] = forward_cost if forward_cost < best_cost

      possible_keys_for_type_and_location(type_name, forward_location).each do |possible_key|
        paths << [*path, { location: forward_location, selection: possible_key, cost: forward_cost }]
      end
    end

    paths.sort! do |a, b|
      cost_diff = a.last[:cost] - b.last[:cost]
      next cost_diff unless cost_diff.zero?
      a.length - b.length
    end.reverse!
  end

  results
end