Class: GraphQL::Stitching::Supergraph

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

Defined Under Namespace

Classes: PathNode

Constant Summary collapse

LOCATION =
"__super"

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.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/graphql/stitching/supergraph.rb', line 38

def initialize(schema:, fields:, boundaries:, executables:)
  @schema = schema
  @boundaries = boundaries
  @possible_keys_by_type = {}
  @possible_keys_by_type_and_location = {}
  @memoized_schema_possible_types = {}
  @memoized_schema_fields = {}

  # add introspection types into the fields mapping
  @locations_by_type_and_field = memoized_introspection_types.each_with_object(fields) do |(type_name, type), memo|
    next unless type.kind.fields?

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

  # validate and normalize executable references
  @executables = executables.each_with_object({ LOCATION => @schema }) do |(location, executable), memo|
    if self.class.validate_executable!(location, executable)
      memo[location.to_s] = executable
    end
  end.freeze
end

Instance Attribute Details

#boundariesObject (readonly)

Returns the value of attribute boundaries.



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

def boundaries
  @boundaries
end

#executablesObject (readonly)

Returns the value of attribute executables.



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

def executables
  @executables
end

#locations_by_type_and_fieldObject (readonly)

Returns the value of attribute locations_by_type_and_field.



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

def locations_by_type_and_field
  @locations_by_type_and_field
end

#schemaObject (readonly)

Returns the value of attribute schema.



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

def schema
  @schema
end

Class Method Details

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



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

def self.from_export(schema:, delegation_map:, executables:)
  schema = GraphQL::Schema.from_definition(schema) if schema.is_a?(String)

  executables = delegation_map["locations"].each_with_object({}) do |location, memo|
    executable = executables[location] || executables[location.to_sym]
    if validate_executable!(location, executable)
      memo[location] = executable
    end
  end

  boundaries = delegation_map["boundaries"].map do |k, b|
    [k, b.map { Boundary.new(**_1) }]
  end

  new(
    schema: schema,
    fields: delegation_map["fields"],
    boundaries: boundaries.to_h,
    executables: executables,
  )
end

.validate_executable!(location, executable) ⇒ Object

Raises:



8
9
10
11
12
# File 'lib/graphql/stitching/supergraph.rb', line 8

def self.validate_executable!(location, executable)
  return true if executable.is_a?(Class) && executable <= GraphQL::Schema
  return true if executable && executable.respond_to?(:call)
  raise StitchingError, "Invalid executable provided for location `#{location}`."
end

Instance Method Details

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



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/graphql/stitching/supergraph.rb', line 108

def execute_at_location(location, source, 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: source,
      variables: variables,
      context: context.frozen? ? context.dup : context,
      validate: false,
    )
  elsif executable.respond_to?(:call)
    executable.call(location, source, variables, context)
  else
    raise StitchingError, "Missing valid executable for #{location} location."
  end
end

#exportObject



71
72
73
74
75
76
77
# File 'lib/graphql/stitching/supergraph.rb', line 71

def export
  return GraphQL::Schema::Printer.print_schema(@schema), {
    "locations" => locations,
    "fields" => fields,
    "boundaries" => @boundaries.map { |k, b| [k, b.map(&:as_json)] }.to_h,
  }
end

#fieldsObject



63
64
65
# File 'lib/graphql/stitching/supergraph.rb', line 63

def fields
  @locations_by_type_and_field.reject { |k, _v| memoized_introspection_types[k] }
end

#fields_by_type_and_locationObject

inverts fields map to provide fields for a type/location “Type” => “location” => [“field1”, “field2”, …]



129
130
131
132
133
134
135
136
137
138
# File 'lib/graphql/stitching/supergraph.rb', line 129

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

#locationsObject



67
68
69
# File 'lib/graphql/stitching/supergraph.rb', line 67

def locations
  @executables.keys.reject { _1 == LOCATION }
end

#locations_by_typeObject

“Type” => [“location1”, “location2”, …]



141
142
143
144
145
# File 'lib/graphql/stitching/supergraph.rb', line 141

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

#memoized_introspection_typesObject



79
80
81
# File 'lib/graphql/stitching/supergraph.rb', line 79

def memoized_introspection_types
  @memoized_introspection_types ||= schema.introspection_system.types
end

#memoized_schema_fields(type_name) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/graphql/stitching/supergraph.rb', line 91

def memoized_schema_fields(type_name)
  @memoized_schema_fields[type_name] ||= begin
    fields = memoized_schema_types[type_name].fields
    @schema.introspection_system.dynamic_fields.each do |field|
      fields[field.name] ||= field # adds __typename
    end

    if type_name == @schema.query.graphql_name
      @schema.introspection_system.entry_points.each do |field|
        fields[field.name] ||= field # adds __schema, __type
      end
    end

    fields
  end
end

#memoized_schema_possible_types(type_name) ⇒ Object



87
88
89
# File 'lib/graphql/stitching/supergraph.rb', line 87

def memoized_schema_possible_types(type_name)
  @memoized_schema_possible_types[type_name] ||= @schema.possible_types(memoized_schema_types[type_name])
end

#memoized_schema_typesObject



83
84
85
# File 'lib/graphql/stitching/supergraph.rb', line 83

def memoized_schema_types
  @memoized_schema_types ||= @schema.types
end

#possible_keys_for_type(type_name) ⇒ Object

collects all possible boundary keys for a given type (“Type”) => [“id”, …]



149
150
151
152
153
# File 'lib/graphql/stitching/supergraph.rb', line 149

def possible_keys_for_type(type_name)
  @possible_keys_by_type[type_name] ||= begin
    @boundaries[type_name].map(&:key).tap(&:uniq!)
  end
end

#possible_keys_for_type_and_location(type_name, location) ⇒ Object

collects possible boundary keys for a given type and location (“Type”, “location”) => [“id”, …]



157
158
159
160
161
162
163
# File 'lib/graphql/stitching/supergraph.rb', line 157

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 & possible_keys_for_type(type_name)
  end
end

#route_type_to_locations(type_name, start_location, goal_locations) ⇒ Object

For a given type, route from one origin location to one or more remote locations used to connect a partial type across locations via boundary queries



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/graphql/stitching/supergraph.rb', line 167

def route_type_to_locations(type_name, start_location, goal_locations)
  if possible_keys_for_type(type_name).length > 1
    # multiple keys use an A* search to traverse intermediary locations
    return route_type_to_locations_via_search(type_name, start_location, goal_locations)
  end

  # types with a single key attribute must all be within a single hop of each other,
  # so can use a simple match to collect boundaries for the goal locations.
  @boundaries[type_name].each_with_object({}) do |boundary, memo|
    if goal_locations.include?(boundary.location)
      memo[boundary.location] = [boundary]
    end
  end
end