graphql 2.4.13 → 2.5.11
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/lib/graphql/analysis/query_complexity.rb +87 -7
- data/lib/graphql/backtrace/table.rb +37 -14
- data/lib/graphql/current.rb +1 -1
- data/lib/graphql/dashboard/detailed_traces.rb +47 -0
- data/lib/graphql/dashboard/installable.rb +22 -0
- data/lib/graphql/dashboard/limiters.rb +93 -0
- data/lib/graphql/dashboard/operation_store.rb +199 -0
- data/lib/graphql/dashboard/statics/charts.min.css +1 -0
- data/lib/graphql/dashboard/statics/dashboard.css +27 -0
- data/lib/graphql/dashboard/statics/dashboard.js +74 -9
- data/lib/graphql/dashboard/subscriptions.rb +96 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
- data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
- data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +49 -1
- data/lib/graphql/dashboard.rb +45 -29
- data/lib/graphql/dataloader/active_record_association_source.rb +28 -8
- data/lib/graphql/dataloader/active_record_source.rb +26 -5
- data/lib/graphql/dataloader/null_dataloader.rb +7 -0
- data/lib/graphql/dataloader/source.rb +16 -4
- data/lib/graphql/dig.rb +2 -1
- data/lib/graphql/execution/interpreter/resolve.rb +3 -3
- data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -1
- data/lib/graphql/execution/interpreter/runtime.rb +163 -59
- data/lib/graphql/execution/interpreter.rb +5 -13
- data/lib/graphql/execution/multiplex.rb +6 -1
- data/lib/graphql/invalid_null_error.rb +15 -2
- data/lib/graphql/language/lexer.rb +9 -2
- data/lib/graphql/language/nodes.rb +5 -1
- data/lib/graphql/language/parser.rb +14 -6
- data/lib/graphql/query/context.rb +3 -8
- data/lib/graphql/query/partial.rb +179 -0
- data/lib/graphql/query.rb +59 -55
- data/lib/graphql/schema/addition.rb +3 -1
- data/lib/graphql/schema/always_visible.rb +1 -0
- data/lib/graphql/schema/argument.rb +9 -3
- data/lib/graphql/schema/build_from_definition.rb +96 -47
- data/lib/graphql/schema/directive/flagged.rb +2 -0
- data/lib/graphql/schema/directive.rb +33 -1
- data/lib/graphql/schema/field.rb +23 -1
- data/lib/graphql/schema/input_object.rb +38 -30
- data/lib/graphql/schema/list.rb +1 -1
- data/lib/graphql/schema/member/has_arguments.rb +2 -2
- data/lib/graphql/schema/member/has_dataloader.rb +4 -2
- data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
- data/lib/graphql/schema/member/has_interfaces.rb +2 -2
- data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
- data/lib/graphql/schema/ractor_shareable.rb +79 -0
- data/lib/graphql/schema/resolver.rb +1 -0
- data/lib/graphql/schema/scalar.rb +1 -6
- data/lib/graphql/schema/timeout.rb +19 -2
- data/lib/graphql/schema/validator/required_validator.rb +15 -6
- data/lib/graphql/schema/visibility/migration.rb +2 -2
- data/lib/graphql/schema/visibility/profile.rb +107 -21
- data/lib/graphql/schema/visibility.rb +41 -29
- data/lib/graphql/schema/warden.rb +13 -5
- data/lib/graphql/schema.rb +228 -32
- data/lib/graphql/static_validation/all_rules.rb +2 -2
- data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
- data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
- data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
- data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
- data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
- data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
- data/lib/graphql/testing/helpers.rb +5 -2
- data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
- data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
- data/lib/graphql/tracing/appsignal_trace.rb +26 -61
- data/lib/graphql/tracing/data_dog_trace.rb +41 -164
- data/lib/graphql/tracing/monitor_trace.rb +283 -0
- data/lib/graphql/tracing/new_relic_trace.rb +34 -164
- data/lib/graphql/tracing/notifications_trace.rb +183 -37
- data/lib/graphql/tracing/null_trace.rb +1 -1
- data/lib/graphql/tracing/perfetto_trace.rb +16 -19
- data/lib/graphql/tracing/prometheus_trace.rb +47 -74
- data/lib/graphql/tracing/scout_trace.rb +25 -59
- data/lib/graphql/tracing/sentry_trace.rb +56 -99
- data/lib/graphql/tracing/statsd_trace.rb +24 -47
- data/lib/graphql/tracing/trace.rb +0 -17
- data/lib/graphql/tracing.rb +1 -0
- data/lib/graphql/type_kinds.rb +1 -0
- data/lib/graphql/version.rb +1 -1
- data/lib/graphql.rb +1 -1
- metadata +35 -26
- data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +0 -63
- data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c2ef756861a779063236ee9f8745d30ae3494138674239c8f638556d7f5105e
|
4
|
+
data.tar.gz: 23592e61f76acb0be2a4c60d953a829e8f1d46534633513ac44797a5d3386d2a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c8db1d976343d569aa1fec1cee02de2c72947e0937fcd9917f8f6e078204efd373d244954e9e03b8dc7fd20066dcf7533bf4b52c706dba0f5640b60ae0465f0
|
7
|
+
data.tar.gz: 38422c543b159cc7243ac4dcc9139ed95b2583cab7942c60e4c557b89d6a4ffaa8fb1df94286fa3fbe4b918f5c116364d5fbe5c2afa611ba9d46d7ac56c18e61
|
@@ -13,7 +13,32 @@ module GraphQL
|
|
13
13
|
|
14
14
|
# Override this method to use the complexity result
|
15
15
|
def result
|
16
|
-
|
16
|
+
case subject.schema.complexity_cost_calculation_mode_for(subject.context)
|
17
|
+
when :future
|
18
|
+
max_possible_complexity
|
19
|
+
when :legacy
|
20
|
+
max_possible_complexity(mode: :legacy)
|
21
|
+
when :compare
|
22
|
+
future_complexity = max_possible_complexity
|
23
|
+
legacy_complexity = max_possible_complexity(mode: :legacy)
|
24
|
+
if future_complexity != legacy_complexity
|
25
|
+
subject.schema.legacy_complexity_cost_calculation_mismatch(subject, future_complexity, legacy_complexity)
|
26
|
+
else
|
27
|
+
future_complexity
|
28
|
+
end
|
29
|
+
when nil
|
30
|
+
subject.logger.warn <<~GRAPHQL
|
31
|
+
GraphQL-Ruby's complexity cost system is getting some "breaking fixes" in a future version. See the migration notes at https://graphql-ruby.org/api-doc/#{GraphQL::VERSION}/GraphQL/Schema.html#complexity_cost_calculation_mode_for-class_method
|
32
|
+
|
33
|
+
To opt into the future behavior, configure your schema (#{subject.schema.name ? subject.schema.name : subject.schema.ancestors}) with:
|
34
|
+
|
35
|
+
complexity_cost_calculation_mode(:future) # or `:legacy`, `:compare`
|
36
|
+
|
37
|
+
GRAPHQL
|
38
|
+
max_possible_complexity(mode: :legacy)
|
39
|
+
else
|
40
|
+
raise ArgumentError, "Expected `:future`, `:legacy`, `:compare`, or `nil` from `#{query.schema}.complexity_cost_calculation_mode_for` but got: #{query.schema.complexity_cost_calculation_mode.inspect}"
|
41
|
+
end
|
17
42
|
end
|
18
43
|
|
19
44
|
# ScopedTypeComplexity models a tree of GraphQL types mapped to inner selections, ie:
|
@@ -44,6 +69,10 @@ module GraphQL
|
|
44
69
|
def own_complexity(child_complexity)
|
45
70
|
@field_definition.calculate_complexity(query: @query, nodes: @nodes, child_complexity: child_complexity)
|
46
71
|
end
|
72
|
+
|
73
|
+
def composite?
|
74
|
+
!empty?
|
75
|
+
end
|
47
76
|
end
|
48
77
|
|
49
78
|
def on_enter_field(node, parent, visitor)
|
@@ -77,16 +106,17 @@ module GraphQL
|
|
77
106
|
private
|
78
107
|
|
79
108
|
# @return [Integer]
|
80
|
-
def max_possible_complexity
|
109
|
+
def max_possible_complexity(mode: :future)
|
81
110
|
@complexities_on_type_by_query.reduce(0) do |total, (query, scopes_stack)|
|
82
|
-
total + merged_max_complexity_for_scopes(query, [scopes_stack.first])
|
111
|
+
total + merged_max_complexity_for_scopes(query, [scopes_stack.first], mode)
|
83
112
|
end
|
84
113
|
end
|
85
114
|
|
86
115
|
# @param query [GraphQL::Query] Used for `query.possible_types`
|
87
116
|
# @param scopes [Array<ScopedTypeComplexity>] Array of scoped type complexities
|
117
|
+
# @param mode [:future, :legacy]
|
88
118
|
# @return [Integer]
|
89
|
-
def merged_max_complexity_for_scopes(query, scopes)
|
119
|
+
def merged_max_complexity_for_scopes(query, scopes, mode)
|
90
120
|
# Aggregate a set of all possible scope types encountered (scope keys).
|
91
121
|
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
92
122
|
possible_scope_types = scopes.each_with_object({}) do |scope, memo|
|
@@ -115,14 +145,20 @@ module GraphQL
|
|
115
145
|
end
|
116
146
|
|
117
147
|
# Find the maximum complexity for the scope type among possible lexical branches.
|
118
|
-
complexity =
|
148
|
+
complexity = case mode
|
149
|
+
when :legacy
|
150
|
+
legacy_merged_max_complexity(query, all_inner_selections)
|
151
|
+
when :future
|
152
|
+
merged_max_complexity(query, all_inner_selections)
|
153
|
+
else
|
154
|
+
raise ArgumentError, "Expected :legacy or :future, not: #{mode.inspect}"
|
155
|
+
end
|
119
156
|
complexity > max ? complexity : max
|
120
157
|
end
|
121
158
|
end
|
122
159
|
|
123
160
|
def types_intersect?(query, a, b)
|
124
161
|
return true if a == b
|
125
|
-
|
126
162
|
a_types = query.types.possible_types(a)
|
127
163
|
query.types.possible_types(b).any? { |t| a_types.include?(t) }
|
128
164
|
end
|
@@ -145,6 +181,50 @@ module GraphQL
|
|
145
181
|
memo.merge!(inner_selection)
|
146
182
|
end
|
147
183
|
|
184
|
+
# Add up the total cost for each unique field name's coalesced selections
|
185
|
+
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
186
|
+
# Collect all child scopes for this field key;
|
187
|
+
# all keys come with at least one scope.
|
188
|
+
child_scopes = inner_selections.filter_map { _1[field_key] }
|
189
|
+
|
190
|
+
# Compute maximum possible cost of child selections;
|
191
|
+
# composites merge their maximums, while leaf scopes are always zero.
|
192
|
+
# FieldsWillMerge validation assures all scopes are uniformly composite or leaf.
|
193
|
+
maximum_children_cost = if child_scopes.any?(&:composite?)
|
194
|
+
merged_max_complexity_for_scopes(query, child_scopes, :future)
|
195
|
+
else
|
196
|
+
0
|
197
|
+
end
|
198
|
+
|
199
|
+
# Identify the maximum cost and scope among possibilities
|
200
|
+
maximum_cost = 0
|
201
|
+
maximum_scope = child_scopes.reduce(child_scopes.last) do |max_scope, possible_scope|
|
202
|
+
scope_cost = possible_scope.own_complexity(maximum_children_cost)
|
203
|
+
if scope_cost > maximum_cost
|
204
|
+
maximum_cost = scope_cost
|
205
|
+
possible_scope
|
206
|
+
else
|
207
|
+
max_scope
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
field_complexity(
|
212
|
+
maximum_scope,
|
213
|
+
max_complexity: maximum_cost,
|
214
|
+
child_complexity: maximum_children_cost,
|
215
|
+
)
|
216
|
+
|
217
|
+
total + maximum_cost
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def legacy_merged_max_complexity(query, inner_selections)
|
222
|
+
# Aggregate a set of all unique field selection keys across all scopes.
|
223
|
+
# Use a hash, but ignore the values; it's just a fast way to work with the keys.
|
224
|
+
unique_field_keys = inner_selections.each_with_object({}) do |inner_selection, memo|
|
225
|
+
memo.merge!(inner_selection)
|
226
|
+
end
|
227
|
+
|
148
228
|
# Add up the total cost for each unique field name's coalesced selections
|
149
229
|
unique_field_keys.each_key.reduce(0) do |total, field_key|
|
150
230
|
composite_scopes = nil
|
@@ -167,7 +247,7 @@ module GraphQL
|
|
167
247
|
end
|
168
248
|
|
169
249
|
if composite_scopes
|
170
|
-
child_complexity = merged_max_complexity_for_scopes(query, composite_scopes)
|
250
|
+
child_complexity = merged_max_complexity_for_scopes(query, composite_scopes, :legacy)
|
171
251
|
|
172
252
|
# This is the last composite scope visited; assume it's representative (for backwards compatibility).
|
173
253
|
# Note: it would be more correct to score each composite scope and use the maximum possibility.
|
@@ -78,23 +78,32 @@ module GraphQL
|
|
78
78
|
end
|
79
79
|
end
|
80
80
|
|
81
|
-
|
82
81
|
object = result.graphql_application_value.object.inspect
|
83
|
-
ast_node =
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
82
|
+
ast_node = nil
|
83
|
+
result.graphql_selections.each do |s|
|
84
|
+
found_ast_node = find_ast_node(s, last_part)
|
85
|
+
if found_ast_node
|
86
|
+
ast_node = found_ast_node
|
87
|
+
break
|
88
|
+
end
|
89
89
|
end
|
90
90
|
|
91
|
-
|
92
|
-
ast_node.
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
91
|
+
if ast_node
|
92
|
+
field_defn = query.get_field(result.graphql_result_type, ast_node.name)
|
93
|
+
args = query.arguments_for(ast_node, field_defn).to_h
|
94
|
+
field_path = field_defn.path
|
95
|
+
if ast_node.alias
|
96
|
+
field_path += " as #{ast_node.alias}"
|
97
|
+
end
|
98
|
+
|
99
|
+
rows << [
|
100
|
+
ast_node.position.join(":"),
|
101
|
+
field_path,
|
102
|
+
"#{object}",
|
103
|
+
args.inspect,
|
104
|
+
inspect_result(@override_value)
|
105
|
+
]
|
106
|
+
end
|
98
107
|
|
99
108
|
rows << HEADERS
|
100
109
|
rows.reverse!
|
@@ -102,6 +111,20 @@ module GraphQL
|
|
102
111
|
end
|
103
112
|
end
|
104
113
|
|
114
|
+
def find_ast_node(node, last_part)
|
115
|
+
return nil unless node
|
116
|
+
return node if node.respond_to?(:alias) && node.respond_to?(:name) && (node.alias == last_part || node.name == last_part)
|
117
|
+
return nil unless node.respond_to?(:selections)
|
118
|
+
return nil if node.selections.nil? || node.selections.empty?
|
119
|
+
|
120
|
+
node.selections.each do |child|
|
121
|
+
child_ast_node = find_ast_node(child, last_part)
|
122
|
+
return child_ast_node if child_ast_node
|
123
|
+
end
|
124
|
+
|
125
|
+
nil
|
126
|
+
end
|
127
|
+
|
105
128
|
# @return [String]
|
106
129
|
def render_table(rows)
|
107
130
|
max = Array.new(HEADERS.length, MIN_COL_WIDTH)
|
data/lib/graphql/current.rb
CHANGED
@@ -22,7 +22,7 @@ module GraphQL
|
|
22
22
|
# ]
|
23
23
|
#
|
24
24
|
module Current
|
25
|
-
# @return [String, nil] Comma-joined operation names for the currently-running {Multiplex}. `nil` if all operations are anonymous.
|
25
|
+
# @return [String, nil] Comma-joined operation names for the currently-running {Execution::Multiplex}. `nil` if all operations are anonymous.
|
26
26
|
def self.operation_name
|
27
27
|
if (m = Fiber[:__graphql_current_multiplex])
|
28
28
|
m.context[:__graphql_current_operation_name] ||= begin
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./installable"
|
3
|
+
module Graphql
|
4
|
+
class Dashboard < Rails::Engine
|
5
|
+
module DetailedTraces
|
6
|
+
class TracesController < Graphql::Dashboard::ApplicationController
|
7
|
+
include Installable
|
8
|
+
|
9
|
+
def index
|
10
|
+
@last = params[:last]&.to_i || 50
|
11
|
+
@before = params[:before]&.to_i
|
12
|
+
@traces = schema_class.detailed_trace.traces(last: @last, before: @before)
|
13
|
+
end
|
14
|
+
|
15
|
+
def show
|
16
|
+
trace = schema_class.detailed_trace.find_trace(params[:id].to_i)
|
17
|
+
send_data(trace.trace_data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def destroy
|
21
|
+
schema_class.detailed_trace.delete_trace(params[:id])
|
22
|
+
flash[:success] = "Trace deleted."
|
23
|
+
head :no_content
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_all
|
27
|
+
schema_class.detailed_trace.delete_all_traces
|
28
|
+
flash[:success] = "Deleted all traces."
|
29
|
+
head :no_content
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def feature_installed?
|
35
|
+
!!schema_class.detailed_trace
|
36
|
+
end
|
37
|
+
|
38
|
+
INSTALLABLE_COMPONENT_HEADER_HTML = "Detailed traces aren't installed yet."
|
39
|
+
INSTALLABLE_COMPONENT_MESSAGE_HTML = <<~HTML.html_safe
|
40
|
+
GraphQL-Ruby can instrument production traffic and save tracing artifacts here for later review.
|
41
|
+
<br>
|
42
|
+
Read more in <a href="https://graphql-ruby.org/queries/tracing#detailed-traces">the detailed tracing docs</a>.
|
43
|
+
HTML
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Graphql
|
3
|
+
class Dashboard < Rails::Engine
|
4
|
+
module Installable
|
5
|
+
def self.included(child_module)
|
6
|
+
child_module.before_action(:check_installed)
|
7
|
+
end
|
8
|
+
|
9
|
+
def feature_installed?
|
10
|
+
raise "Implement #{self.class}#feature_installed? to check whether this should render `not_installed` or not."
|
11
|
+
end
|
12
|
+
|
13
|
+
def check_installed
|
14
|
+
if !feature_installed?
|
15
|
+
@component_header_html = self.class::INSTALLABLE_COMPONENT_HEADER_HTML
|
16
|
+
@component_message_html = self.class::INSTALLABLE_COMPONENT_MESSAGE_HTML
|
17
|
+
render "graphql/dashboard/not_installed"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./installable"
|
3
|
+
module Graphql
|
4
|
+
class Dashboard < Rails::Engine
|
5
|
+
module Limiters
|
6
|
+
class LimitersController < Dashboard::ApplicationController
|
7
|
+
include Installable
|
8
|
+
FALLBACK_CSP_NONCE_GENERATOR = ->(_req) { SecureRandom.hex(32) }
|
9
|
+
|
10
|
+
def show
|
11
|
+
name = params[:name]
|
12
|
+
@title = case name
|
13
|
+
when "runtime"
|
14
|
+
"Runtime Limiter"
|
15
|
+
when "active_operations"
|
16
|
+
"Active Operation Limiter"
|
17
|
+
when "mutations"
|
18
|
+
"Mutation Limiter"
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Unknown limiter name: #{name}"
|
21
|
+
end
|
22
|
+
|
23
|
+
limiter = limiter_for(name)
|
24
|
+
if limiter.nil?
|
25
|
+
@install_path = "http://graphql-ruby.org/limiters/#{name}"
|
26
|
+
else
|
27
|
+
@chart_mode = params[:chart] || "day"
|
28
|
+
@current_soft = limiter.soft_limit_enabled?
|
29
|
+
@histogram = limiter.dashboard_histogram(@chart_mode)
|
30
|
+
|
31
|
+
# These configs may have already been defined by the application; provide overrides here if not.
|
32
|
+
request.content_security_policy_nonce_generator ||= FALLBACK_CSP_NONCE_GENERATOR
|
33
|
+
nonce_dirs = request.content_security_policy_nonce_directives || []
|
34
|
+
if !nonce_dirs.include?("style-src")
|
35
|
+
nonce_dirs += ["style-src"]
|
36
|
+
request.content_security_policy_nonce_directives = nonce_dirs
|
37
|
+
end
|
38
|
+
@csp_nonce = request.content_security_policy_nonce
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def update
|
43
|
+
name = params[:name]
|
44
|
+
limiter = limiter_for(name)
|
45
|
+
if limiter
|
46
|
+
limiter.toggle_soft_limit
|
47
|
+
flash[:success] = if limiter.soft_limit_enabled?
|
48
|
+
"Enabled soft limiting -- over-limit traffic will be logged but not rejected."
|
49
|
+
else
|
50
|
+
"Disabled soft limiting -- over-limit traffic will be rejected."
|
51
|
+
end
|
52
|
+
else
|
53
|
+
flash[:warning] = "No limiter configured for #{name.inspect}"
|
54
|
+
end
|
55
|
+
|
56
|
+
redirect_to graphql_dashboard.limiters_limiter_path(name, chart: params[:chart])
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def limiter_for(name)
|
62
|
+
case name
|
63
|
+
when "runtime"
|
64
|
+
schema_class.enterprise_runtime_limiter
|
65
|
+
when "active_operations"
|
66
|
+
schema_class.enterprise_active_operation_limiter
|
67
|
+
when "mutations"
|
68
|
+
schema_class.enterprise_mutation_limiter
|
69
|
+
else
|
70
|
+
raise ArgumentError, "Unknown limiter: #{name}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def feature_installed?
|
75
|
+
defined?(GraphQL::Enterprise::Limiter) &&
|
76
|
+
(
|
77
|
+
schema_class.enterprise_active_operation_limiter ||
|
78
|
+
schema_class.enterprise_runtime_limiter ||
|
79
|
+
(schema_class.respond_to?(:enterprise_mutation_limiter) && schema_class.enterprise_mutation_limiter)
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
INSTALLABLE_COMPONENT_HEADER_HTML = "Rate limiters aren't installed on this schema yet."
|
85
|
+
INSTALLABLE_COMPONENT_MESSAGE_HTML = <<-HTML.html_safe
|
86
|
+
Check out the docs to get started with GraphQL-Enterprise's
|
87
|
+
<a href="https://graphql-ruby.org/limiters/runtime.html">runtime limiter</a> or
|
88
|
+
<a href="https://graphql-ruby.org/limiters/active_operations.html">active operation limiter</a>.
|
89
|
+
HTML
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative "./installable"
|
3
|
+
module Graphql
|
4
|
+
class Dashboard < Rails::Engine
|
5
|
+
module OperationStore
|
6
|
+
class BaseController < Dashboard::ApplicationController
|
7
|
+
include Installable
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def feature_installed?
|
12
|
+
schema_class.respond_to?(:operation_store) && schema_class.operation_store.is_a?(GraphQL::Pro::OperationStore)
|
13
|
+
end
|
14
|
+
|
15
|
+
INSTALLABLE_COMPONENT_HEADER_HTML = "<code>OperationStore</code> isn't installed for this schema yet.".html_safe
|
16
|
+
INSTALLABLE_COMPONENT_MESSAGE_HTML = <<-HTML.html_safe
|
17
|
+
Learn more about improving performance and security with stored operations
|
18
|
+
in the <a href="https://graphql-ruby.org/operation_store/overview.html"><code>OperationStore</code> docs</a>.
|
19
|
+
HTML
|
20
|
+
end
|
21
|
+
|
22
|
+
class ClientsController < BaseController
|
23
|
+
def index
|
24
|
+
@order_by = params[:order_by] || "name"
|
25
|
+
@order_dir = params[:order_dir].presence || "asc"
|
26
|
+
clients_page = schema_class.operation_store.all_clients(
|
27
|
+
page: params[:page]&.to_i || 1,
|
28
|
+
per_page: params[:per_page]&.to_i || 25,
|
29
|
+
order_by: @order_by,
|
30
|
+
order_dir: @order_dir,
|
31
|
+
)
|
32
|
+
|
33
|
+
@clients_page = clients_page
|
34
|
+
end
|
35
|
+
|
36
|
+
def new
|
37
|
+
@client = init_client(secret: SecureRandom.hex(32))
|
38
|
+
end
|
39
|
+
|
40
|
+
def create
|
41
|
+
client_params = params.require(:client).permit(:name, :secret)
|
42
|
+
schema_class.operation_store.upsert_client(client_params[:name], client_params[:secret])
|
43
|
+
flash[:success] = "Created #{client_params[:name].inspect}"
|
44
|
+
redirect_to graphql_dashboard.operation_store_clients_path
|
45
|
+
end
|
46
|
+
|
47
|
+
def edit
|
48
|
+
@client = schema_class.operation_store.get_client(params[:name])
|
49
|
+
end
|
50
|
+
|
51
|
+
def update
|
52
|
+
client_name = params[:name]
|
53
|
+
client_secret = params.require(:client).permit(:secret)[:secret]
|
54
|
+
schema_class.operation_store.upsert_client(client_name, client_secret)
|
55
|
+
flash[:success] = "Updated #{client_name.inspect}"
|
56
|
+
redirect_to graphql_dashboard.operation_store_clients_path
|
57
|
+
end
|
58
|
+
|
59
|
+
def destroy
|
60
|
+
client_name = params[:name]
|
61
|
+
schema_class.operation_store.delete_client(client_name)
|
62
|
+
flash[:success] = "Deleted #{client_name.inspect}"
|
63
|
+
redirect_to graphql_dashboard.operation_store_clients_path
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def init_client(name: nil, secret: nil)
|
69
|
+
GraphQL::Pro::OperationStore::ClientRecord.new(
|
70
|
+
name: name,
|
71
|
+
secret: secret,
|
72
|
+
created_at: nil,
|
73
|
+
operations_count: 0,
|
74
|
+
archived_operations_count: 0,
|
75
|
+
last_synced_at: nil,
|
76
|
+
last_used_at: nil,
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class OperationsController < BaseController
|
82
|
+
def index
|
83
|
+
@client_operations = client_name = params[:client_name]
|
84
|
+
per_page = params[:per_page]&.to_i || 25
|
85
|
+
page = params[:page]&.to_i || 1
|
86
|
+
@is_archived = params[:archived_status] == :archived
|
87
|
+
order_by = params[:order_by] || "name"
|
88
|
+
order_dir = params[:order_dir]&.to_sym || :asc
|
89
|
+
if @client_operations
|
90
|
+
@operations_page = schema_class.operation_store.get_client_operations_by_client(
|
91
|
+
client_name,
|
92
|
+
page: page,
|
93
|
+
per_page: per_page,
|
94
|
+
is_archived: @is_archived,
|
95
|
+
order_by: order_by,
|
96
|
+
order_dir: order_dir,
|
97
|
+
)
|
98
|
+
opposite_archive_mode_count = schema_class.operation_store.get_client_operations_by_client(
|
99
|
+
client_name,
|
100
|
+
page: 1,
|
101
|
+
per_page: 1,
|
102
|
+
is_archived: !@is_archived,
|
103
|
+
order_by: order_by,
|
104
|
+
order_dir: order_dir,
|
105
|
+
).total_count
|
106
|
+
else
|
107
|
+
@operations_page = schema_class.operation_store.all_operations(
|
108
|
+
page: page,
|
109
|
+
per_page: per_page,
|
110
|
+
is_archived: @is_archived,
|
111
|
+
order_by: order_by,
|
112
|
+
order_dir: order_dir,
|
113
|
+
)
|
114
|
+
opposite_archive_mode_count = schema_class.operation_store.all_operations(
|
115
|
+
page: 1,
|
116
|
+
per_page: 1,
|
117
|
+
is_archived: !@is_archived,
|
118
|
+
order_by: order_by,
|
119
|
+
order_dir: order_dir,
|
120
|
+
).total_count
|
121
|
+
end
|
122
|
+
|
123
|
+
if @is_archived
|
124
|
+
@archived_operations_count = @operations_page.total_count
|
125
|
+
@unarchived_operations_count = opposite_archive_mode_count
|
126
|
+
else
|
127
|
+
@archived_operations_count = opposite_archive_mode_count
|
128
|
+
@unarchived_operations_count = @operations_page.total_count
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def show
|
133
|
+
digest = params[:digest]
|
134
|
+
@operation = schema_class.operation_store.get_operation_by_digest(digest)
|
135
|
+
if @operation
|
136
|
+
# Parse & re-format the query
|
137
|
+
document = GraphQL.parse(@operation.body)
|
138
|
+
@graphql_source = document.to_query_string
|
139
|
+
|
140
|
+
@client_operations = schema_class.operation_store.get_client_operations_by_digest(digest)
|
141
|
+
@entries = schema_class.operation_store.get_index_entries_by_digest(digest)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def update
|
146
|
+
is_archived = case params[:modification]
|
147
|
+
when :archive
|
148
|
+
true
|
149
|
+
when :unarchive
|
150
|
+
false
|
151
|
+
else
|
152
|
+
raise ArgumentError, "Unexpected modification: #{params[:modification].inspect}"
|
153
|
+
end
|
154
|
+
|
155
|
+
if (client_name = params[:client_name])
|
156
|
+
operation_aliases = params[:operation_aliases]
|
157
|
+
schema_class.operation_store.archive_client_operations(
|
158
|
+
client_name: client_name,
|
159
|
+
operation_aliases: operation_aliases,
|
160
|
+
is_archived: is_archived
|
161
|
+
)
|
162
|
+
flash[:success] = "#{is_archived ? "Archived" : "Activated"} #{operation_aliases.size} #{"operation".pluralize(operation_aliases.size)}"
|
163
|
+
else
|
164
|
+
digests = params[:digests]
|
165
|
+
schema_class.operation_store.archive_operations(
|
166
|
+
digests: digests,
|
167
|
+
is_archived: is_archived
|
168
|
+
)
|
169
|
+
flash[:success] = "#{is_archived ? "Archived" : "Activated"} #{digests.size} #{"operation".pluralize(digests.size)}"
|
170
|
+
end
|
171
|
+
head :no_content
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
class IndexEntriesController < BaseController
|
176
|
+
def index
|
177
|
+
@search_term = if request.params["q"] && request.params["q"].length > 0
|
178
|
+
request.params["q"]
|
179
|
+
else
|
180
|
+
nil
|
181
|
+
end
|
182
|
+
|
183
|
+
@index_entries_page = schema_class.operation_store.all_index_entries(
|
184
|
+
search_term: @search_term,
|
185
|
+
page: params[:page]&.to_i || 1,
|
186
|
+
per_page: params[:per_page]&.to_i || 25,
|
187
|
+
)
|
188
|
+
end
|
189
|
+
|
190
|
+
def show
|
191
|
+
name = params[:name]
|
192
|
+
@entry = schema_class.operation_store.index.get_entry(name)
|
193
|
+
@chain = schema_class.operation_store.index.index_entry_chain(name)
|
194
|
+
@operations = schema_class.operation_store.get_operations_by_index_entry(name)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|