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.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/query_complexity.rb +87 -7
  3. data/lib/graphql/backtrace/table.rb +37 -14
  4. data/lib/graphql/current.rb +1 -1
  5. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  6. data/lib/graphql/dashboard/installable.rb +22 -0
  7. data/lib/graphql/dashboard/limiters.rb +93 -0
  8. data/lib/graphql/dashboard/operation_store.rb +199 -0
  9. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  10. data/lib/graphql/dashboard/statics/dashboard.css +27 -0
  11. data/lib/graphql/dashboard/statics/dashboard.js +74 -9
  12. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  13. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  14. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  15. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  16. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  17. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  18. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  19. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  20. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  21. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  22. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  23. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  24. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  25. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  26. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  27. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +49 -1
  28. data/lib/graphql/dashboard.rb +45 -29
  29. data/lib/graphql/dataloader/active_record_association_source.rb +28 -8
  30. data/lib/graphql/dataloader/active_record_source.rb +26 -5
  31. data/lib/graphql/dataloader/null_dataloader.rb +7 -0
  32. data/lib/graphql/dataloader/source.rb +16 -4
  33. data/lib/graphql/dig.rb +2 -1
  34. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  35. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -1
  36. data/lib/graphql/execution/interpreter/runtime.rb +163 -59
  37. data/lib/graphql/execution/interpreter.rb +5 -13
  38. data/lib/graphql/execution/multiplex.rb +6 -1
  39. data/lib/graphql/invalid_null_error.rb +15 -2
  40. data/lib/graphql/language/lexer.rb +9 -2
  41. data/lib/graphql/language/nodes.rb +5 -1
  42. data/lib/graphql/language/parser.rb +14 -6
  43. data/lib/graphql/query/context.rb +3 -8
  44. data/lib/graphql/query/partial.rb +179 -0
  45. data/lib/graphql/query.rb +59 -55
  46. data/lib/graphql/schema/addition.rb +3 -1
  47. data/lib/graphql/schema/always_visible.rb +1 -0
  48. data/lib/graphql/schema/argument.rb +9 -3
  49. data/lib/graphql/schema/build_from_definition.rb +96 -47
  50. data/lib/graphql/schema/directive/flagged.rb +2 -0
  51. data/lib/graphql/schema/directive.rb +33 -1
  52. data/lib/graphql/schema/field.rb +23 -1
  53. data/lib/graphql/schema/input_object.rb +38 -30
  54. data/lib/graphql/schema/list.rb +1 -1
  55. data/lib/graphql/schema/member/has_arguments.rb +2 -2
  56. data/lib/graphql/schema/member/has_dataloader.rb +4 -2
  57. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  58. data/lib/graphql/schema/member/has_interfaces.rb +2 -2
  59. data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
  60. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  61. data/lib/graphql/schema/resolver.rb +1 -0
  62. data/lib/graphql/schema/scalar.rb +1 -6
  63. data/lib/graphql/schema/timeout.rb +19 -2
  64. data/lib/graphql/schema/validator/required_validator.rb +15 -6
  65. data/lib/graphql/schema/visibility/migration.rb +2 -2
  66. data/lib/graphql/schema/visibility/profile.rb +107 -21
  67. data/lib/graphql/schema/visibility.rb +41 -29
  68. data/lib/graphql/schema/warden.rb +13 -5
  69. data/lib/graphql/schema.rb +228 -32
  70. data/lib/graphql/static_validation/all_rules.rb +2 -2
  71. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +47 -13
  72. data/lib/graphql/static_validation/rules/fields_will_merge.rb +78 -16
  73. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +10 -2
  74. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  75. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  76. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  77. data/lib/graphql/testing/helpers.rb +5 -2
  78. data/lib/graphql/tracing/active_support_notifications_trace.rb +7 -0
  79. data/lib/graphql/tracing/appoptics_tracing.rb +5 -0
  80. data/lib/graphql/tracing/appsignal_trace.rb +26 -61
  81. data/lib/graphql/tracing/data_dog_trace.rb +41 -164
  82. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  83. data/lib/graphql/tracing/new_relic_trace.rb +34 -164
  84. data/lib/graphql/tracing/notifications_trace.rb +183 -37
  85. data/lib/graphql/tracing/null_trace.rb +1 -1
  86. data/lib/graphql/tracing/perfetto_trace.rb +16 -19
  87. data/lib/graphql/tracing/prometheus_trace.rb +47 -74
  88. data/lib/graphql/tracing/scout_trace.rb +25 -59
  89. data/lib/graphql/tracing/sentry_trace.rb +56 -99
  90. data/lib/graphql/tracing/statsd_trace.rb +24 -47
  91. data/lib/graphql/tracing/trace.rb +0 -17
  92. data/lib/graphql/tracing.rb +1 -0
  93. data/lib/graphql/type_kinds.rb +1 -0
  94. data/lib/graphql/version.rb +1 -1
  95. data/lib/graphql.rb +1 -1
  96. metadata +35 -26
  97. data/lib/graphql/dashboard/views/graphql/dashboard/traces/index.html.erb +0 -63
  98. 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: 35661aff446f955116332f131cd6323839e0b5f0ea104512a8257a28d3232e43
4
- data.tar.gz: 023eb7cff94490520342572da1b7dc2b017dfa99daa5c9d18503b51de7dccc66
3
+ metadata.gz: 5c2ef756861a779063236ee9f8745d30ae3494138674239c8f638556d7f5105e
4
+ data.tar.gz: 23592e61f76acb0be2a4c60d953a829e8f1d46534633513ac44797a5d3386d2a
5
5
  SHA512:
6
- metadata.gz: 29645fc0f923d6e268888c43fbd703189160ac8be5ea8d0fc9a17683d7336c43b20cfcdfd1cafd90291840c59e2d06d3eb8b27b66bb41e26beea0f4388e7a839
7
- data.tar.gz: 63f1a5c91cbd08948ffc96bcb0d581a3959c2d0f13be19402200d1e0209187ab70d6f8c27e2b6a62ac93a27a945792d93ecf03bb6f53b37778eb88c3570e2553
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
- max_possible_complexity
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 = merged_max_complexity(query, all_inner_selections)
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 = result.graphql_selections.find { |s| s.alias == last_part || s.name == last_part }
84
- field_defn = query.get_field(result.graphql_result_type, ast_node.name)
85
- args = query.arguments_for(ast_node, field_defn).to_h
86
- field_path = field_defn.path
87
- if ast_node.alias
88
- field_path += " as #{ast_node.alias}"
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
- rows << [
92
- ast_node.position.join(":"),
93
- field_path,
94
- "#{object}",
95
- args.inspect,
96
- inspect_result(@override_value)
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)
@@ -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