graphql-stitching 1.7.2 → 1.8.0
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 +4 -4
- data/README.md +1 -1
- data/Rakefile +11 -1
- data/benchmark/run.rb +279 -0
- data/docs/performance.md +28 -0
- data/graphql-stitching.gemspec +3 -3
- data/lib/graphql/stitching/client.rb +2 -2
- data/lib/graphql/stitching/composer/type_resolver_config.rb +1 -1
- data/lib/graphql/stitching/composer.rb +1 -3
- data/lib/graphql/stitching/executor/path_access.rb +81 -0
- data/lib/graphql/stitching/executor/root_source.rb +59 -40
- data/lib/graphql/stitching/executor/shaper.rb +27 -18
- data/lib/graphql/stitching/executor/type_resolver_source.rb +78 -88
- data/lib/graphql/stitching/executor.rb +2 -1
- data/lib/graphql/stitching/http_executable.rb +1 -2
- data/lib/graphql/stitching/plan.rb +46 -13
- data/lib/graphql/stitching/planner.rb +62 -46
- data/lib/graphql/stitching/request/skip_include.rb +2 -2
- data/lib/graphql/stitching/request.rb +1 -1
- data/lib/graphql/stitching/supergraph.rb +16 -6
- data/lib/graphql/stitching/type_resolver.rb +12 -1
- data/lib/graphql/stitching/util.rb +21 -4
- data/lib/graphql/stitching/version.rb +1 -1
- metadata +11 -9
- /data/docs/{introduction.md → README.md} +0 -0
|
@@ -10,6 +10,15 @@ module GraphQL
|
|
|
10
10
|
SUPERGRAPH_LOCATIONS = [Supergraph::SUPERGRAPH_LOCATION].freeze
|
|
11
11
|
ROOT_INDEX = 0
|
|
12
12
|
|
|
13
|
+
class ScopePartition
|
|
14
|
+
attr_reader :location, :selections
|
|
15
|
+
|
|
16
|
+
def initialize(location:, selections:)
|
|
17
|
+
@location = location
|
|
18
|
+
@selections = selections
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
13
22
|
def initialize(request)
|
|
14
23
|
@request = request
|
|
15
24
|
@supergraph = request.supergraph
|
|
@@ -76,11 +85,15 @@ module GraphQL
|
|
|
76
85
|
resolver: nil
|
|
77
86
|
)
|
|
78
87
|
# coalesce repeat parameters into a single entrypoint
|
|
79
|
-
entrypoint =
|
|
88
|
+
entrypoint = String.new
|
|
89
|
+
entrypoint << parent_index.to_s << "/" << location << "/" << parent_type.graphql_name
|
|
90
|
+
entrypoint << "/" << (resolver&.key&.to_s || "") << "/#"
|
|
91
|
+
path.each { entrypoint << "/" << _1 }
|
|
92
|
+
|
|
80
93
|
step = @steps_by_entrypoint[entrypoint]
|
|
81
|
-
next_index = step ?
|
|
94
|
+
next_index = step ? step.index : @planning_index += 1
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
unless selections.empty?
|
|
84
97
|
selections = extract_locale_selections(location, parent_type, next_index, selections, path, variables)
|
|
85
98
|
end
|
|
86
99
|
|
|
@@ -97,13 +110,12 @@ module GraphQL
|
|
|
97
110
|
resolver: resolver,
|
|
98
111
|
)
|
|
99
112
|
else
|
|
113
|
+
step.variables.merge!(variables)
|
|
100
114
|
step.selections.concat(selections)
|
|
101
115
|
step
|
|
102
116
|
end
|
|
103
117
|
end
|
|
104
118
|
|
|
105
|
-
ScopePartition = Struct.new(:location, :selections, keyword_init: true)
|
|
106
|
-
|
|
107
119
|
# A) Group all root selections by their preferred entrypoint locations.
|
|
108
120
|
def build_root_entrypoints
|
|
109
121
|
parent_type = @request.query.root_type_for_operation(@request.operation.operation_type)
|
|
@@ -111,10 +123,9 @@ module GraphQL
|
|
|
111
123
|
case @request.operation.operation_type
|
|
112
124
|
when QUERY_OP
|
|
113
125
|
# A.1) Group query fields by location for parallel execution.
|
|
114
|
-
selections_by_location = {}
|
|
126
|
+
selections_by_location = Hash.new { |h, k| h[k] = [] }
|
|
115
127
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
|
116
128
|
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
|
117
|
-
selections_by_location[locations.first] ||= []
|
|
118
129
|
selections_by_location[locations.first] << node
|
|
119
130
|
end
|
|
120
131
|
|
|
@@ -132,7 +143,8 @@ module GraphQL
|
|
|
132
143
|
# A.2) Partition mutation fields by consecutive location for serial execution.
|
|
133
144
|
partitions = []
|
|
134
145
|
each_field_in_scope(parent_type, @request.operation.selections) do |node|
|
|
135
|
-
|
|
146
|
+
locations = @supergraph.locations_by_type_and_field[parent_type.graphql_name][node.name] || SUPERGRAPH_LOCATIONS
|
|
147
|
+
next_location = locations.first
|
|
136
148
|
|
|
137
149
|
if partitions.none? || partitions.last.location != next_location
|
|
138
150
|
partitions << ScopePartition.new(location: next_location, selections: [])
|
|
@@ -213,7 +225,7 @@ module GraphQL
|
|
|
213
225
|
input_selections.each do |node|
|
|
214
226
|
case node
|
|
215
227
|
when GraphQL::Language::Nodes::Field
|
|
216
|
-
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node != TypeResolver::TYPENAME_EXPORT_NODE
|
|
228
|
+
if node.alias&.start_with?(TypeResolver::EXPORT_PREFIX) && node.object_id != TypeResolver::TYPENAME_EXPORT_NODE.object_id
|
|
217
229
|
raise StitchingError, %(Alias "#{node.alias}" is not allowed because "#{TypeResolver::EXPORT_PREFIX}" is a reserved prefix.)
|
|
218
230
|
elsif node.name == TYPENAME
|
|
219
231
|
locale_selections << node
|
|
@@ -228,8 +240,10 @@ module GraphQL
|
|
|
228
240
|
end
|
|
229
241
|
|
|
230
242
|
# B.3) Collect all variable definitions used within the filtered selection.
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
extract_node_argument_variables(node, locale_variables)
|
|
244
|
+
extract_node_directive_variables(node, locale_variables)
|
|
245
|
+
schema_fields = @supergraph.memoized_schema_fields(parent_type.graphql_name)
|
|
246
|
+
field_type = schema_fields[node.name].type.unwrap
|
|
233
247
|
|
|
234
248
|
if Util.is_leaf_type?(field_type)
|
|
235
249
|
locale_selections << node
|
|
@@ -245,7 +259,8 @@ module GraphQL
|
|
|
245
259
|
fragment_type = node.type ? @supergraph.memoized_schema_types[node.type.name] : parent_type
|
|
246
260
|
next unless @supergraph.locations_by_type[fragment_type.graphql_name].include?(current_location)
|
|
247
261
|
|
|
248
|
-
|
|
262
|
+
extract_node_directive_variables(node, locale_variables)
|
|
263
|
+
is_same_scope = fragment_type == parent_type && node.directives.empty?
|
|
249
264
|
selection_set = is_same_scope ? locale_selections : []
|
|
250
265
|
extract_locale_selections(current_location, fragment_type, parent_index, node.selections, path, locale_variables, selection_set)
|
|
251
266
|
|
|
@@ -258,14 +273,17 @@ module GraphQL
|
|
|
258
273
|
fragment = @request.fragment_definitions[node.name]
|
|
259
274
|
next unless @supergraph.locations_by_type[fragment.type.name].include?(current_location)
|
|
260
275
|
|
|
276
|
+
extract_node_directive_variables(node, locale_variables)
|
|
277
|
+
extract_node_directive_variables(fragment, locale_variables)
|
|
261
278
|
requires_typename = true
|
|
262
279
|
fragment_type = @supergraph.memoized_schema_types[fragment.type.name]
|
|
263
|
-
|
|
280
|
+
directives = fragment.directives.empty? && node.directives.empty? ? EMPTY_ARRAY : fragment.directives + node.directives
|
|
281
|
+
is_same_scope = fragment_type == parent_type && directives.empty?
|
|
264
282
|
selection_set = is_same_scope ? locale_selections : []
|
|
265
283
|
extract_locale_selections(current_location, fragment_type, parent_index, fragment.selections, path, locale_variables, selection_set)
|
|
266
284
|
|
|
267
285
|
unless is_same_scope
|
|
268
|
-
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, selections: selection_set)
|
|
286
|
+
locale_selections << GraphQL::Language::Nodes::InlineFragment.new(type: fragment.type, directives: directives, selections: selection_set)
|
|
269
287
|
end
|
|
270
288
|
|
|
271
289
|
else
|
|
@@ -344,20 +362,28 @@ module GraphQL
|
|
|
344
362
|
|
|
345
363
|
# B.3) Collect all variable definitions used within the filtered selection.
|
|
346
364
|
# These specify which request variables to pass along with each step.
|
|
347
|
-
def
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
when GraphQL::Language::Nodes::InputObject
|
|
351
|
-
extract_node_variables(argument.value, variable_definitions)
|
|
352
|
-
when GraphQL::Language::Nodes::VariableIdentifier
|
|
353
|
-
variable_definitions[argument.value.name] ||= @request.variable_definitions[argument.value.name]
|
|
354
|
-
end
|
|
355
|
-
end
|
|
365
|
+
def extract_node_argument_variables(node, variable_definitions)
|
|
366
|
+
arguments = node.arguments
|
|
367
|
+
return if arguments.empty?
|
|
356
368
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
369
|
+
arguments.each { |argument| extract_value_variables(argument.value, variable_definitions) }
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def extract_node_directive_variables(node, variable_definitions)
|
|
373
|
+
directives = node.directives
|
|
374
|
+
return if directives.empty?
|
|
375
|
+
|
|
376
|
+
directives.each { |directive| extract_node_argument_variables(directive, variable_definitions) }
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def extract_value_variables(value, variable_definitions)
|
|
380
|
+
case value
|
|
381
|
+
when GraphQL::Language::Nodes::InputObject
|
|
382
|
+
extract_node_argument_variables(value, variable_definitions)
|
|
383
|
+
when GraphQL::Language::Nodes::VariableIdentifier
|
|
384
|
+
variable_definitions[value.name] ||= @request.variable_definitions[value.name]
|
|
385
|
+
when Array
|
|
386
|
+
value.each { extract_value_variables(_1, variable_definitions) }
|
|
361
387
|
end
|
|
362
388
|
end
|
|
363
389
|
|
|
@@ -377,9 +403,11 @@ module GraphQL
|
|
|
377
403
|
end
|
|
378
404
|
|
|
379
405
|
# C.2) Distribute non-unique fields among locations that were added during C.1.
|
|
380
|
-
if selections_by_location.
|
|
406
|
+
if !selections_by_location.empty? && !remote_selections.empty?
|
|
407
|
+
available_locations = Set.new(selections_by_location.each_key)
|
|
408
|
+
|
|
381
409
|
remote_selections.reject! do |node|
|
|
382
|
-
used_location = possible_locations_by_field[node.name].find {
|
|
410
|
+
used_location = possible_locations_by_field[node.name].find { available_locations.include?(_1) }
|
|
383
411
|
if used_location
|
|
384
412
|
selections_by_location[used_location] << node
|
|
385
413
|
true
|
|
@@ -388,29 +416,17 @@ module GraphQL
|
|
|
388
416
|
end
|
|
389
417
|
|
|
390
418
|
# C.3) Distribute remaining fields among locations weighted by greatest availability.
|
|
391
|
-
if remote_selections.
|
|
392
|
-
field_count_by_location =
|
|
419
|
+
if !remote_selections.empty?
|
|
420
|
+
field_count_by_location = Hash.new(0)
|
|
421
|
+
remote_selections.each do |node|
|
|
393
422
|
possible_locations_by_field[node.name].each do |location|
|
|
394
|
-
|
|
395
|
-
memo[location] += 1
|
|
423
|
+
field_count_by_location[location] += 1
|
|
396
424
|
end
|
|
397
425
|
end
|
|
398
426
|
|
|
399
427
|
remote_selections.each do |node|
|
|
400
428
|
possible_locations = possible_locations_by_field[node.name]
|
|
401
|
-
preferred_location = possible_locations.first
|
|
402
|
-
|
|
403
|
-
possible_locations.reduce(0) do |max_availability, possible_location|
|
|
404
|
-
availability = field_count_by_location.fetch(possible_location, 0)
|
|
405
|
-
|
|
406
|
-
if availability > max_availability
|
|
407
|
-
preferred_location = possible_location
|
|
408
|
-
availability
|
|
409
|
-
else
|
|
410
|
-
max_availability
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
|
|
429
|
+
preferred_location = possible_locations.max_by { field_count_by_location[_1] } || possible_locations.first
|
|
414
430
|
selections_by_location[preferred_location] ||= []
|
|
415
431
|
selections_by_location[preferred_location] << node
|
|
416
432
|
end
|
|
@@ -34,7 +34,7 @@ module GraphQL::Stitching
|
|
|
34
34
|
next nil
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
node = render_node(node, variables)
|
|
37
|
+
node = render_node(node, variables) unless node.selections.empty?
|
|
38
38
|
changed ||= node.object_id != original_node.object_id
|
|
39
39
|
node
|
|
40
40
|
end
|
|
@@ -51,7 +51,7 @@ module GraphQL::Stitching
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
def prune_node(node, variables)
|
|
54
|
-
return node
|
|
54
|
+
return node if node.directives.empty?
|
|
55
55
|
|
|
56
56
|
delete_node = false
|
|
57
57
|
filtered_directives = node.directives.reject do |directive|
|
|
@@ -101,7 +101,7 @@ module GraphQL
|
|
|
101
101
|
|
|
102
102
|
# @return [String] A string of directives applied to the root operation. These are passed through in all subgraph requests.
|
|
103
103
|
def operation_directives
|
|
104
|
-
@operation_directives ||=
|
|
104
|
+
@operation_directives ||= unless operation.directives.empty?
|
|
105
105
|
printer = GraphQL::Language::Printer.new
|
|
106
106
|
operation.directives.map { printer.print(_1) }.join(" ")
|
|
107
107
|
end
|
|
@@ -50,11 +50,11 @@ module GraphQL
|
|
|
50
50
|
end
|
|
51
51
|
end.freeze
|
|
52
52
|
|
|
53
|
-
if visibility_profiles.
|
|
53
|
+
if visibility_profiles.empty?
|
|
54
|
+
@schema.use(GraphQL::Schema::AlwaysVisible)
|
|
55
|
+
else
|
|
54
56
|
profiles = visibility_profiles.each_with_object({ nil => {} }) { |p, m| m[p.to_s] = {} }
|
|
55
57
|
@schema.use(GraphQL::Schema::Visibility, profiles: profiles)
|
|
56
|
-
else
|
|
57
|
-
@schema.use(GraphQL::Schema::AlwaysVisible)
|
|
58
58
|
end
|
|
59
59
|
end
|
|
60
60
|
|
|
@@ -187,7 +187,17 @@ module GraphQL
|
|
|
187
187
|
|
|
188
188
|
private
|
|
189
189
|
|
|
190
|
-
PathNode
|
|
190
|
+
class PathNode
|
|
191
|
+
attr_reader :location, :key, :resolver
|
|
192
|
+
attr_accessor :cost
|
|
193
|
+
|
|
194
|
+
def initialize(location:, key:, resolver: nil, cost: 0)
|
|
195
|
+
@location = location
|
|
196
|
+
@key = key
|
|
197
|
+
@resolver = resolver
|
|
198
|
+
@cost = cost
|
|
199
|
+
end
|
|
200
|
+
end
|
|
191
201
|
|
|
192
202
|
# tunes A* search to favor paths with fewest joining locations, ie:
|
|
193
203
|
# favor longer paths through target locations over shorter paths with additional locations.
|
|
@@ -196,10 +206,10 @@ module GraphQL
|
|
|
196
206
|
costs = {}
|
|
197
207
|
|
|
198
208
|
paths = possible_keys_for_type_and_location(type_name, start_location).map do |possible_key|
|
|
199
|
-
[PathNode.new(location: start_location, key: possible_key
|
|
209
|
+
[PathNode.new(location: start_location, key: possible_key)]
|
|
200
210
|
end
|
|
201
211
|
|
|
202
|
-
while paths.
|
|
212
|
+
while !paths.empty?
|
|
203
213
|
path = paths.pop
|
|
204
214
|
current_location = path.last.location
|
|
205
215
|
current_key = path.last.key
|
|
@@ -10,6 +10,13 @@ module GraphQL
|
|
|
10
10
|
extend ArgumentsParser
|
|
11
11
|
extend KeysParser
|
|
12
12
|
|
|
13
|
+
class << self
|
|
14
|
+
# only intended for testing...
|
|
15
|
+
def use_static_version?
|
|
16
|
+
@use_static_version ||= false
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
# location name providing the resolver query.
|
|
14
21
|
attr_reader :location
|
|
15
22
|
|
|
@@ -47,7 +54,11 @@ module GraphQL
|
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
def version
|
|
50
|
-
@version ||=
|
|
57
|
+
@version ||= if self.class.use_static_version?
|
|
58
|
+
[location, field, key.to_definition, type_name].join(".")
|
|
59
|
+
else
|
|
60
|
+
Stitching.digest.call("#{Stitching::VERSION}/#{as_json.to_json}")
|
|
61
|
+
end
|
|
51
62
|
end
|
|
52
63
|
|
|
53
64
|
def ==(other)
|
|
@@ -4,12 +4,29 @@ module GraphQL
|
|
|
4
4
|
module Stitching
|
|
5
5
|
# General utilities to aid with stitching.
|
|
6
6
|
class Util
|
|
7
|
-
TypeStructure
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
class TypeStructure
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
def initialize(list:, null:, name:)
|
|
11
|
+
@list = list
|
|
12
|
+
@null = null
|
|
13
|
+
@name = name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def list?
|
|
17
|
+
@list
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def null?
|
|
21
|
+
@null
|
|
22
|
+
end
|
|
10
23
|
|
|
11
24
|
def non_null?
|
|
12
|
-
|
|
25
|
+
!@null
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ==(other)
|
|
29
|
+
@list == other.list? && @null == other.null? && @name == other.name
|
|
13
30
|
end
|
|
14
31
|
end
|
|
15
32
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphql-stitching
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Greg MacWilliam
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-06-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: graphql
|
|
@@ -28,42 +28,42 @@ dependencies:
|
|
|
28
28
|
name: bundler
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- - "
|
|
31
|
+
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
33
|
version: '2.0'
|
|
34
34
|
type: :development
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- - "
|
|
38
|
+
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '2.0'
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rake
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
|
-
- - "
|
|
45
|
+
- - ">="
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
47
|
version: '12.0'
|
|
48
48
|
type: :development
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
|
-
- - "
|
|
52
|
+
- - ">="
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '12.0'
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - ">="
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
61
|
version: '5.12'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - ">="
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '5.12'
|
|
69
69
|
description: Combine GraphQL services into one unified graph
|
|
@@ -79,13 +79,14 @@ files:
|
|
|
79
79
|
- LICENSE
|
|
80
80
|
- README.md
|
|
81
81
|
- Rakefile
|
|
82
|
+
- benchmark/run.rb
|
|
83
|
+
- docs/README.md
|
|
82
84
|
- docs/composing_a_supergraph.md
|
|
83
85
|
- docs/error_handling.md
|
|
84
86
|
- docs/executables.md
|
|
85
87
|
- docs/images/library.png
|
|
86
88
|
- docs/images/merging.png
|
|
87
89
|
- docs/images/stitching.png
|
|
88
|
-
- docs/introduction.md
|
|
89
90
|
- docs/merged_types.md
|
|
90
91
|
- docs/merged_types_apollo.md
|
|
91
92
|
- docs/performance.md
|
|
@@ -167,6 +168,7 @@ files:
|
|
|
167
168
|
- lib/graphql/stitching/composer/validate_type_resolvers.rb
|
|
168
169
|
- lib/graphql/stitching/directives.rb
|
|
169
170
|
- lib/graphql/stitching/executor.rb
|
|
171
|
+
- lib/graphql/stitching/executor/path_access.rb
|
|
170
172
|
- lib/graphql/stitching/executor/root_source.rb
|
|
171
173
|
- lib/graphql/stitching/executor/shaper.rb
|
|
172
174
|
- lib/graphql/stitching/executor/type_resolver_source.rb
|
|
File without changes
|