graphql 2.4.3 → 2.5.2

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/lib/graphql/analysis/analyzer.rb +2 -1
  3. data/lib/graphql/analysis/visitor.rb +38 -41
  4. data/lib/graphql/analysis.rb +15 -12
  5. data/lib/graphql/autoload.rb +38 -0
  6. data/lib/graphql/backtrace/table.rb +118 -55
  7. data/lib/graphql/backtrace.rb +1 -19
  8. data/lib/graphql/current.rb +6 -1
  9. data/lib/graphql/dashboard/detailed_traces.rb +47 -0
  10. data/lib/graphql/dashboard/installable.rb +22 -0
  11. data/lib/graphql/dashboard/limiters.rb +93 -0
  12. data/lib/graphql/dashboard/operation_store.rb +199 -0
  13. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.css +6 -0
  14. data/lib/graphql/dashboard/statics/bootstrap-5.3.3.min.js +7 -0
  15. data/lib/graphql/dashboard/statics/charts.min.css +1 -0
  16. data/lib/graphql/dashboard/statics/dashboard.css +30 -0
  17. data/lib/graphql/dashboard/statics/dashboard.js +143 -0
  18. data/lib/graphql/dashboard/statics/header-icon.png +0 -0
  19. data/lib/graphql/dashboard/statics/icon.png +0 -0
  20. data/lib/graphql/dashboard/subscriptions.rb +96 -0
  21. data/lib/graphql/dashboard/views/graphql/dashboard/detailed_traces/traces/index.html.erb +45 -0
  22. data/lib/graphql/dashboard/views/graphql/dashboard/landings/show.html.erb +18 -0
  23. data/lib/graphql/dashboard/views/graphql/dashboard/limiters/limiters/show.html.erb +62 -0
  24. data/lib/graphql/dashboard/views/graphql/dashboard/not_installed.html.erb +18 -0
  25. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +23 -0
  26. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +21 -0
  27. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +69 -0
  28. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +7 -0
  29. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +39 -0
  30. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/show.html.erb +32 -0
  31. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/index.html.erb +81 -0
  32. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +71 -0
  33. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/subscriptions/show.html.erb +41 -0
  34. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/index.html.erb +55 -0
  35. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +40 -0
  36. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +108 -0
  37. data/lib/graphql/dashboard.rb +158 -0
  38. data/lib/graphql/dataloader/active_record_association_source.rb +64 -0
  39. data/lib/graphql/dataloader/active_record_source.rb +26 -0
  40. data/lib/graphql/dataloader/async_dataloader.rb +21 -9
  41. data/lib/graphql/dataloader/null_dataloader.rb +1 -1
  42. data/lib/graphql/dataloader/source.rb +3 -3
  43. data/lib/graphql/dataloader.rb +43 -14
  44. data/lib/graphql/execution/interpreter/resolve.rb +3 -3
  45. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +34 -4
  46. data/lib/graphql/execution/interpreter/runtime.rb +94 -51
  47. data/lib/graphql/execution/interpreter.rb +16 -7
  48. data/lib/graphql/execution/multiplex.rb +1 -5
  49. data/lib/graphql/introspection/directive_location_enum.rb +1 -1
  50. data/lib/graphql/invalid_name_error.rb +1 -1
  51. data/lib/graphql/invalid_null_error.rb +5 -15
  52. data/lib/graphql/language/cache.rb +13 -0
  53. data/lib/graphql/language/document_from_schema_definition.rb +8 -7
  54. data/lib/graphql/language/lexer.rb +11 -4
  55. data/lib/graphql/language/nodes.rb +3 -0
  56. data/lib/graphql/language/parser.rb +15 -8
  57. data/lib/graphql/language/printer.rb +8 -8
  58. data/lib/graphql/language/static_visitor.rb +37 -33
  59. data/lib/graphql/language/visitor.rb +59 -55
  60. data/lib/graphql/pagination/connection.rb +1 -1
  61. data/lib/graphql/query/context/scoped_context.rb +1 -1
  62. data/lib/graphql/query/context.rb +6 -5
  63. data/lib/graphql/query/variable_validation_error.rb +1 -1
  64. data/lib/graphql/query.rb +19 -23
  65. data/lib/graphql/railtie.rb +7 -0
  66. data/lib/graphql/schema/addition.rb +1 -1
  67. data/lib/graphql/schema/argument.rb +7 -8
  68. data/lib/graphql/schema/build_from_definition.rb +99 -53
  69. data/lib/graphql/schema/directive/flagged.rb +3 -1
  70. data/lib/graphql/schema/directive.rb +2 -2
  71. data/lib/graphql/schema/enum.rb +36 -1
  72. data/lib/graphql/schema/enum_value.rb +1 -1
  73. data/lib/graphql/schema/field/scope_extension.rb +1 -1
  74. data/lib/graphql/schema/field.rb +27 -13
  75. data/lib/graphql/schema/field_extension.rb +1 -1
  76. data/lib/graphql/schema/has_single_input_argument.rb +3 -1
  77. data/lib/graphql/schema/input_object.rb +77 -40
  78. data/lib/graphql/schema/interface.rb +3 -2
  79. data/lib/graphql/schema/loader.rb +1 -1
  80. data/lib/graphql/schema/member/has_arguments.rb +25 -17
  81. data/lib/graphql/schema/member/has_dataloader.rb +60 -0
  82. data/lib/graphql/schema/member/has_deprecation_reason.rb +15 -0
  83. data/lib/graphql/schema/member/has_directives.rb +4 -4
  84. data/lib/graphql/schema/member/has_fields.rb +19 -1
  85. data/lib/graphql/schema/member/has_interfaces.rb +5 -5
  86. data/lib/graphql/schema/member/has_validators.rb +1 -1
  87. data/lib/graphql/schema/member/scoped.rb +1 -1
  88. data/lib/graphql/schema/member/type_system_helpers.rb +1 -1
  89. data/lib/graphql/schema/member.rb +1 -0
  90. data/lib/graphql/schema/object.rb +25 -8
  91. data/lib/graphql/schema/relay_classic_mutation.rb +0 -1
  92. data/lib/graphql/schema/resolver.rb +12 -10
  93. data/lib/graphql/schema/subscription.rb +52 -6
  94. data/lib/graphql/schema/union.rb +1 -1
  95. data/lib/graphql/schema/validator/required_validator.rb +23 -6
  96. data/lib/graphql/schema/validator.rb +1 -1
  97. data/lib/graphql/schema/visibility/migration.rb +1 -0
  98. data/lib/graphql/schema/visibility/profile.rb +95 -243
  99. data/lib/graphql/schema/visibility/visit.rb +190 -0
  100. data/lib/graphql/schema/visibility.rb +169 -28
  101. data/lib/graphql/schema/warden.rb +18 -5
  102. data/lib/graphql/schema.rb +93 -44
  103. data/lib/graphql/static_validation/all_rules.rb +1 -1
  104. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +1 -1
  105. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +1 -1
  106. data/lib/graphql/static_validation/rules/fields_will_merge.rb +1 -1
  107. data/lib/graphql/static_validation/rules/no_definitions_are_present.rb +1 -1
  108. data/lib/graphql/static_validation/rules/not_single_subscription_error.rb +25 -0
  109. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +1 -1
  110. data/lib/graphql/static_validation/rules/subscription_root_exists_and_single_subscription_selection.rb +26 -0
  111. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +1 -1
  112. data/lib/graphql/static_validation/rules/variable_names_are_unique.rb +1 -1
  113. data/lib/graphql/static_validation/rules/variable_usages_are_allowed.rb +1 -1
  114. data/lib/graphql/static_validation/validation_context.rb +1 -0
  115. data/lib/graphql/static_validation/validator.rb +6 -1
  116. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -1
  117. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +12 -10
  118. data/lib/graphql/subscriptions/event.rb +12 -1
  119. data/lib/graphql/subscriptions/serialize.rb +1 -1
  120. data/lib/graphql/subscriptions.rb +1 -1
  121. data/lib/graphql/testing/helpers.rb +7 -4
  122. data/lib/graphql/tracing/active_support_notifications_trace.rb +14 -3
  123. data/lib/graphql/tracing/active_support_notifications_tracing.rb +1 -1
  124. data/lib/graphql/tracing/appoptics_trace.rb +9 -1
  125. data/lib/graphql/tracing/appoptics_tracing.rb +7 -0
  126. data/lib/graphql/tracing/appsignal_trace.rb +32 -55
  127. data/lib/graphql/tracing/appsignal_tracing.rb +2 -0
  128. data/lib/graphql/tracing/call_legacy_tracers.rb +66 -0
  129. data/lib/graphql/tracing/data_dog_trace.rb +46 -158
  130. data/lib/graphql/tracing/data_dog_tracing.rb +2 -0
  131. data/lib/graphql/tracing/detailed_trace/memory_backend.rb +60 -0
  132. data/lib/graphql/tracing/detailed_trace/redis_backend.rb +72 -0
  133. data/lib/graphql/tracing/detailed_trace.rb +93 -0
  134. data/lib/graphql/tracing/legacy_hooks_trace.rb +1 -0
  135. data/lib/graphql/tracing/legacy_trace.rb +4 -61
  136. data/lib/graphql/tracing/monitor_trace.rb +283 -0
  137. data/lib/graphql/tracing/new_relic_trace.rb +47 -54
  138. data/lib/graphql/tracing/new_relic_tracing.rb +2 -0
  139. data/lib/graphql/tracing/notifications_trace.rb +182 -34
  140. data/lib/graphql/tracing/notifications_tracing.rb +2 -0
  141. data/lib/graphql/tracing/null_trace.rb +9 -0
  142. data/lib/graphql/tracing/perfetto_trace/trace.proto +141 -0
  143. data/lib/graphql/tracing/perfetto_trace/trace_pb.rb +33 -0
  144. data/lib/graphql/tracing/perfetto_trace.rb +734 -0
  145. data/lib/graphql/tracing/platform_trace.rb +5 -0
  146. data/lib/graphql/tracing/prometheus_trace/graphql_collector.rb +2 -0
  147. data/lib/graphql/tracing/prometheus_trace.rb +72 -68
  148. data/lib/graphql/tracing/prometheus_tracing.rb +2 -0
  149. data/lib/graphql/tracing/scout_trace.rb +32 -55
  150. data/lib/graphql/tracing/scout_tracing.rb +2 -0
  151. data/lib/graphql/tracing/sentry_trace.rb +62 -94
  152. data/lib/graphql/tracing/statsd_trace.rb +33 -41
  153. data/lib/graphql/tracing/statsd_tracing.rb +2 -0
  154. data/lib/graphql/tracing/trace.rb +111 -1
  155. data/lib/graphql/tracing.rb +31 -30
  156. data/lib/graphql/types/relay/connection_behaviors.rb +3 -3
  157. data/lib/graphql/types/relay/edge_behaviors.rb +2 -2
  158. data/lib/graphql/types.rb +18 -11
  159. data/lib/graphql/version.rb +1 -1
  160. data/lib/graphql.rb +55 -47
  161. metadata +146 -11
  162. data/lib/graphql/backtrace/inspect_result.rb +0 -38
  163. data/lib/graphql/backtrace/trace.rb +0 -93
  164. data/lib/graphql/backtrace/tracer.rb +0 -80
  165. data/lib/graphql/schema/null_mask.rb +0 -11
  166. data/lib/graphql/static_validation/rules/subscription_root_exists.rb +0 -17
@@ -0,0 +1,55 @@
1
+ <% content_for(:title, "Subscriptions - Topics") %>
2
+ <div class="row mt-3 justify-content-between">
3
+ <div class="col-auto">
4
+ <h3>
5
+ <%= pluralize(@all_topics_count, "Subscription Topic") %>
6
+ </h3>
7
+ </div>
8
+ <div class="col-auto">
9
+ <%= button_tag "Clear All", class: "btn btn-outline-danger", data: { subscriptions_delete_all: graphql_dashboard.subscriptions_clear_all_path } %>
10
+ </div>
11
+ </div>
12
+
13
+ <table class="table table-striped">
14
+ <thead>
15
+ <tr>
16
+ <th>Name</th>
17
+ <th># Subscriptions</th>
18
+ <th>Last Triggered At</th>
19
+ </tr>
20
+ </thead>
21
+ <tbody>
22
+ <% if @all_topics_count == 0 %>
23
+ <tr>
24
+ <td colspan="3" class="text-center">
25
+ <em>There aren't any subscriptions right now.</em>
26
+ </td>
27
+ </tr>
28
+ <% else %>
29
+ <% @topics.each do |topic| %>
30
+ <tr>
31
+ <td><%= link_to(topic.name, graphql_dashboard.subscriptions_topic_path(name: topic.name)) %></td>
32
+ <td><%= topic.subscriptions_count %></td>
33
+ <td><%= topic.last_triggered_at || "--" %></td>
34
+ </tr>
35
+ <% end %>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+
40
+ <div class="row">
41
+ <div class="col-auto">
42
+ <% if @page > 1 %>
43
+ <%= link_to("« prev", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page - 1), class: "btn btn-outline-secondary") %>
44
+ <% else %>
45
+ <button class="btn btn-outline-secondary" disabled>« prev</button>
46
+ <% end %>
47
+ </div>
48
+ <div class="col-auto">
49
+ <% if @has_next_page %>
50
+ <%= link_to("next »", graphql_dashboard.subscriptions_topics_path(per_page: params[:per_page], page: @page + 1), class: "btn btn-outline-secondary") %>
51
+ <% else %>
52
+ <button class="btn btn-outline-secondary" disabled>next »</button>
53
+ <% end %>
54
+ </div>
55
+ </div>
@@ -0,0 +1,40 @@
1
+ <%= content_for(:title, "Subscriptions - #{params[:name]}") %>
2
+ <div class="row mt-3">
3
+ <div class="col">
4
+ <h3>Topic: <code><%= params[:name] %></code></h3>
5
+ </div>
6
+ </div>
7
+
8
+ <div class="row">
9
+ <div class="col">
10
+ <p>Last triggered: <%= @topic_last_triggered_at || "none" %></p>
11
+ <p><%= pluralize(@subscriptions_count, "Subscription") %></p>
12
+ <div>
13
+ </div>
14
+
15
+ <div class="row">
16
+ <div class="col">
17
+ <table class="table table-striped">
18
+ <thead>
19
+ <tr>
20
+ <th>Subscription ID</th>
21
+ <th>Created At</th>
22
+ <th>Subscribed?</th>
23
+ <th>Broadcast?</th>
24
+ <% if @show_broadcast_subscribers_count %><th>Subscribers</th><% end %>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% @subscriptions.each do |subscription| %>
29
+ <tr>
30
+ <td><%= link_to(subscription[:id], graphql_dashboard.subscriptions_subscription_path(subscription[:id])) %></td>
31
+ <td><%= subscription[:created_at] %></td>
32
+ <td><code><%= subscription[:still_subscribed] ? "YES" : "NO" %></code></td>
33
+ <td><code><%= subscription[:is_broadcast] ? "YES" : "NO" %></code></td>
34
+ <% if @show_broadcast_subscribers_count %><td><%= subscription[:subscribers_count] %></td><% end %>
35
+ </tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ </div>
@@ -0,0 +1,108 @@
1
+ <!doctype html>
2
+ <html lang="en" class="h-100" >
3
+ <head>
4
+ <link rel="icon" type="image/png" href="<%= graphql_dashboard.static_path("icon.png") %>" />
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>GraphQL Dashboard <%= content_for?(:title) ? " · #{content_for(:title)}" : "" %> </title>
8
+ <%= stylesheet_link_tag graphql_dashboard.static_path("bootstrap-5.3.3.min.css") %>
9
+ <%= stylesheet_link_tag graphql_dashboard.static_path("charts.min.css") %>
10
+ <%= stylesheet_link_tag graphql_dashboard.static_path("dashboard.css") %>
11
+ <%= javascript_include_tag graphql_dashboard.static_path("bootstrap-5.3.3.min.js") %>
12
+ <%= javascript_include_tag graphql_dashboard.static_path("dashboard.js") %>
13
+ <%= csrf_meta_tags %>
14
+ </head>
15
+ <body class="h-100 d-flex flex-column">
16
+ <main class="flex-shrink-0">
17
+ <div class="container-fluid">
18
+ <div class="row">
19
+ <div class="col gx-0">
20
+ <nav class="navbar navbar-expand-lg bg-body-tertiary">
21
+ <div class="container-fluid">
22
+ <%= link_to graphql_dashboard.root_path, class: "navbar-brand" do %>
23
+ <img id="header-icon" src="<%= graphql_dashboard.static_path("header-icon.png") %>" alt="GraphQL-Ruby" />
24
+ <% end %>
25
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
26
+ <span class="navbar-toggler-icon"></span>
27
+ </button>
28
+ <div class="collapse navbar-collapse" id="navbarSupportedContent">
29
+ <ul class="navbar-nav me-auto mb-2 mb-lg-0">
30
+ <li class="nav-item">
31
+ <%= link_to "Traces", graphql_dashboard.detailed_traces_traces_path, class: "nav-link #{params[:controller] == "graphql/dashboard/detailed_traces" ? "active" : ""}" %>
32
+ </li>
33
+ <li class="nav-item dropdown">
34
+ <a class="nav-link dropdown-toggle <%= params[:controller].include?("operation_store") ? "active" : "" %>" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
35
+ Persisted Operations
36
+ </a>
37
+ <ul class="dropdown-menu">
38
+ <li>
39
+ <%= link_to "Clients", graphql_dashboard.operation_store_clients_path, class: "dropdown-item" %>
40
+ </li>
41
+ <li>
42
+ <%= link_to "Operations", graphql_dashboard.operation_store_operations_path, class: "dropdown-item" %>
43
+ </li>
44
+ <li>
45
+ <%= link_to "Index", graphql_dashboard.operation_store_index_entries_path, class: "dropdown-item" %>
46
+ </li>
47
+ </ul>
48
+ </li>
49
+ <li class="nav-item">
50
+ <%= link_to "Subscriptions", graphql_dashboard.subscriptions_topics_path, class: "nav-link #{params[:controller] == "graphql/dashboard/subscriptions" ? "active" : ""}" %>
51
+ </li>
52
+ <li class="nav-item dropdown">
53
+ <a class="nav-link dropdown-toggle <%= params[:controller].include?("limiters") ? "active" : "" %>" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
54
+ Rate Limiters
55
+ </a>
56
+ <ul class="dropdown-menu">
57
+ <li>
58
+ <%= link_to "Runtime", graphql_dashboard.limiters_limiter_path("runtime", chart: params[:chart]), class: "dropdown-item" %>
59
+ </li>
60
+ <li>
61
+ <%= link_to "Active Operations", graphql_dashboard.limiters_limiter_path("active_operations", chart: params[:chart]), class: "dropdown-item" %>
62
+ </li>
63
+ <% if schema_class.respond_to?(:enterprise_mutation_limiter) && schema_class.enterprise_mutation_limiter %>
64
+ <li>
65
+ <%= link_to "Mutations", graphql_dashboard.limiters_limiter_path("mutations", chart: params[:chart]), class: "dropdown-item" %>
66
+ </li>
67
+ <% end %>
68
+ </ul>
69
+ </li>
70
+ </ul>
71
+ <span class="navbar-text pe-2">
72
+ <%= link_to ".", "#", class: "nav-link", id: "themeToggle" %>
73
+ </span>
74
+ </div>
75
+ </div>
76
+ </nav>
77
+ </div>
78
+ </div>
79
+ <% flash.each do |flash_type, flash_message| %>
80
+ <div class="row mt-2">
81
+ <div class="col">
82
+ <div class="alert alert-<%= flash_type %> alert-dismissible fade show" role="alert">
83
+ <%= flash_message %>
84
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ <% end %>
89
+ </div>
90
+ <div class="container-fluid">
91
+ <%= yield %>
92
+ </div>
93
+ </main>
94
+ <footer class="mt-auto">
95
+ <div class="container-fluid">
96
+ <div class="sticky-bottom">
97
+ <div class="row bg-body-tertiary">
98
+ <div class="col gx-0 px-4">
99
+ <p class="fs-6 text-center pt-2 text-muted">
100
+ <em>GraphQL-Ruby v<%= GraphQL::VERSION %></em> · <code><%= schema_class %></code>
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </footer>
107
+ </body>
108
+ </html>
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/engine'
3
+ module Graphql
4
+ # `GraphQL::Dashboard` is a `Rails::Engine`-based dashboard for viewing metadata about your GraphQL schema.
5
+ #
6
+ # Pass the class name of your schema when mounting it.
7
+ # @see GraphQL::Tracing::DetailedTrace DetailedTrace for viewing production traces in the Dashboard
8
+ #
9
+ # @example Mounting the Dashboard in your app
10
+ # mount GraphQL::Dashboard, at: "graphql_dashboard", schema: "MySchema"
11
+ #
12
+ # @example Authenticating the Dashboard with HTTP Basic Auth
13
+ # # config/initializers/graphql_dashboard.rb
14
+ # GraphQL::Dashboard.middleware.use(Rack::Auth::Basic) do |username, password|
15
+ # # Compare the provided username/password to an application setting:
16
+ # ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, username) &&
17
+ # ActiveSupport::SecurityUtils.secure_compare(Rails.application.credentials.graphql_dashboard_username, password)
18
+ # end
19
+ #
20
+ # @example Custom Rails authentication
21
+ # # config/initializers/graphql_dashboard.rb
22
+ # ActiveSupport.on_load(:graphql_dashboard_application_controller) do
23
+ # # context here is GraphQL::Dashboard::ApplicationController
24
+ #
25
+ # before_action do
26
+ # raise ActionController::RoutingError.new('Not Found') unless current_user&.admin?
27
+ # end
28
+ #
29
+ # def current_user
30
+ # # load current user
31
+ # end
32
+ # end
33
+ #
34
+ class Dashboard < Rails::Engine
35
+ engine_name "graphql_dashboard"
36
+ isolate_namespace(Graphql::Dashboard)
37
+ routes.draw do
38
+ root "landings#show"
39
+ resources :statics, only: :show, constraints: { id: /[0-9A-Za-z\-.]+/ }
40
+
41
+ namespace :detailed_traces do
42
+ resources :traces, only: [:index, :show, :destroy] do
43
+ collection do
44
+ delete :delete_all, to: "traces#delete_all", as: :delete_all
45
+ end
46
+ end
47
+ end
48
+
49
+ namespace :limiters do
50
+ resources :limiters, only: [:show, :update], param: :name
51
+ end
52
+
53
+ namespace :operation_store do
54
+ resources :clients, param: :name do
55
+ resources :operations, param: :digest, only: [:index] do
56
+ collection do
57
+ get :archived, to: "operations#index", archived_status: :archived, as: :archived
58
+ post :archive, to: "operations#update", modification: :archive, as: :archive
59
+ post :unarchive, to: "operations#update", modification: :unarchive, as: :unarchive
60
+ end
61
+ end
62
+ end
63
+
64
+ resources :operations, param: :digest, only: [:index, :show] do
65
+ collection do
66
+ get :archived, to: "operations#index", archived_status: :archived, as: :archived
67
+ post :archive, to: "operations#update", modification: :archive, as: :archive
68
+ post :unarchive, to: "operations#update", modification: :unarchive, as: :unarchive
69
+ end
70
+ end
71
+ resources :index_entries, only: [:index, :show], param: :name, constraints: { name: /[A-Za-z0-9_.]+/}
72
+ end
73
+
74
+ namespace :subscriptions do
75
+ resources :topics, only: [:index, :show], param: :name, constraints: { name: /.*/ }
76
+ resources :subscriptions, only: [:show], constraints: { id: /[a-zA-Z0-9\-]+/ }
77
+ post "/subscriptions/clear_all", to: "subscriptions#clear_all", as: :clear_all
78
+ end
79
+ end
80
+
81
+ class ApplicationController < ActionController::Base
82
+ protect_from_forgery with: :exception
83
+ prepend_view_path(File.join(__FILE__, "../dashboard/views"))
84
+
85
+ content_security_policy do |policy|
86
+ policy.default_src(:self) if policy.default_src(*policy.default_src).blank?
87
+ policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
88
+ policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
89
+ policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
90
+ policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
91
+ policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
92
+ policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
93
+ policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
94
+ policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
95
+ policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
96
+ end
97
+
98
+ def schema_class
99
+ @schema_class ||= begin
100
+ schema_param = request.query_parameters["schema"] || params[:schema]
101
+ case schema_param
102
+ when Class
103
+ schema_param
104
+ when String
105
+ schema_param.constantize
106
+ else
107
+ raise "Missing `params[:schema]`, please provide a class or string to `mount GraphQL::Dashboard, schema: ...`"
108
+ end
109
+ end
110
+ end
111
+ helper_method :schema_class
112
+ end
113
+
114
+ class LandingsController < ApplicationController
115
+ def show
116
+ end
117
+ end
118
+
119
+ class StaticsController < ApplicationController
120
+ skip_after_action :verify_same_origin_request
121
+ # Use an explicit list of files to avoid any chance of reading other files from disk
122
+ STATICS = {}
123
+
124
+ [
125
+ "icon.png",
126
+ "header-icon.png",
127
+ "charts.min.css",
128
+ "dashboard.css",
129
+ "dashboard.js",
130
+ "bootstrap-5.3.3.min.css",
131
+ "bootstrap-5.3.3.min.js",
132
+ ].each do |static_file|
133
+ STATICS[static_file] = File.expand_path("../dashboard/statics/#{static_file}", __FILE__)
134
+ end
135
+
136
+ def show
137
+ expires_in 1.year, public: true
138
+ if (filepath = STATICS[params[:id]])
139
+ render file: filepath
140
+ else
141
+ head :not_found
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ require 'graphql/dashboard/detailed_traces'
149
+ require 'graphql/dashboard/limiters'
150
+ require 'graphql/dashboard/operation_store'
151
+ require 'graphql/dashboard/subscriptions'
152
+
153
+ # Rails expects the engine to be called `Graphql::Dashboard`,
154
+ # but `GraphQL::Dashboard` is consistent with this gem's naming.
155
+ # So define both constants to refer to the same class.
156
+ GraphQL::Dashboard = Graphql::Dashboard
157
+
158
+ ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/dataloader/source"
3
+ require "graphql/dataloader/active_record_source"
4
+
5
+ module GraphQL
6
+ class Dataloader
7
+ class ActiveRecordAssociationSource < GraphQL::Dataloader::Source
8
+ RECORD_SOURCE_CLASS = ActiveRecordSource
9
+
10
+ def initialize(association, scope = nil)
11
+ @association = association
12
+ @scope = scope
13
+ end
14
+
15
+ def load(record)
16
+ if (assoc = record.association(@association)).loaded?
17
+ assoc.target
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def fetch(records)
24
+ record_classes = Set.new.compare_by_identity
25
+ associated_classes = Set.new.compare_by_identity
26
+ records.each do |record|
27
+ if record_classes.add?(record.class)
28
+ reflection = record.class.reflect_on_association(@association)
29
+ if !reflection.polymorphic? && reflection.klass
30
+ associated_classes.add(reflection.klass)
31
+ end
32
+ end
33
+ end
34
+
35
+ available_records = []
36
+ associated_classes.each do |assoc_class|
37
+ already_loaded_records = dataloader.with(RECORD_SOURCE_CLASS, assoc_class).results.values
38
+ available_records.concat(already_loaded_records)
39
+ end
40
+
41
+ ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association, available_records: available_records, scope: @scope).call
42
+
43
+ loaded_associated_records = records.map { |r| r.public_send(@association) }
44
+ records_by_model = {}
45
+ loaded_associated_records.each do |record|
46
+ if record
47
+ updates = records_by_model[record.class] ||= {}
48
+ updates[record.id] = record
49
+ end
50
+ end
51
+
52
+ if @scope.nil?
53
+ # Don't cache records loaded via scope because they might have reduced `SELECT`s
54
+ # Could check .select_values here?
55
+ records_by_model.each do |model_class, updates|
56
+ dataloader.with(RECORD_SOURCE_CLASS, model_class).merge(updates)
57
+ end
58
+ end
59
+
60
+ loaded_associated_records
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require "graphql/dataloader/source"
3
+
4
+ module GraphQL
5
+ class Dataloader
6
+ class ActiveRecordSource < GraphQL::Dataloader::Source
7
+ def initialize(model_class, find_by: model_class.primary_key)
8
+ @model_class = model_class
9
+ @find_by = find_by
10
+ @type_for_column = @model_class.type_for_attribute(@find_by)
11
+ end
12
+
13
+ def load(requested_key)
14
+ casted_key = @type_for_column.cast(requested_key)
15
+ super(casted_key)
16
+ end
17
+
18
+ def fetch(record_ids)
19
+ records = @model_class.where(@find_by => record_ids)
20
+ record_lookup = {}
21
+ records.each { |r| record_lookup[r.public_send(@find_by)] = r }
22
+ record_ids.map { |id| record_lookup[id] }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -2,16 +2,20 @@
2
2
  module GraphQL
3
3
  class Dataloader
4
4
  class AsyncDataloader < Dataloader
5
- def yield
6
- if (condition = Thread.current[:graphql_dataloader_next_tick])
5
+ def yield(source = Fiber[:__graphql_current_dataloader_source])
6
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
7
+ trace&.dataloader_fiber_yield(source)
8
+ if (condition = Fiber[:graphql_dataloader_next_tick])
7
9
  condition.wait
8
10
  else
9
11
  Fiber.yield
10
12
  end
13
+ trace&.dataloader_fiber_resume(source)
11
14
  nil
12
15
  end
13
16
 
14
17
  def run
18
+ trace = Fiber[:__graphql_current_multiplex]&.current_trace
15
19
  jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
16
20
  job_fibers = []
17
21
  next_job_fibers = []
@@ -20,11 +24,12 @@ module GraphQL
20
24
  first_pass = true
21
25
  sources_condition = Async::Condition.new
22
26
  manager = spawn_fiber do
23
- while first_pass || job_fibers.any?
27
+ trace&.begin_dataloader(self)
28
+ while first_pass || !job_fibers.empty?
24
29
  first_pass = false
25
30
  fiber_vars = get_fiber_variables
26
31
 
27
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber)))
32
+ while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
28
33
  if f.alive?
29
34
  finished = run_fiber(f)
30
35
  if !finished
@@ -37,8 +42,8 @@ module GraphQL
37
42
 
38
43
  Sync do |root_task|
39
44
  set_fiber_variables(fiber_vars)
40
- while source_tasks.any? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
41
- while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition))))
45
+ while !source_tasks.empty? || @source_cache.each_value.any? { |group_sources| group_sources.each_value.any?(&:pending?) }
46
+ while (task = (source_tasks.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size + next_source_tasks.size) < total_fiber_limit) && spawn_source_task(root_task, sources_condition, trace))))
42
47
  if task.alive?
43
48
  root_task.yield # give the source task a chance to run
44
49
  next_source_tasks << task
@@ -50,6 +55,7 @@ module GraphQL
50
55
  end
51
56
  end
52
57
  end
58
+ trace&.end_dataloader(self)
53
59
  end
54
60
 
55
61
  manager.resume
@@ -63,7 +69,7 @@ module GraphQL
63
69
 
64
70
  private
65
71
 
66
- def spawn_source_task(parent_task, condition)
72
+ def spawn_source_task(parent_task, condition, trace)
67
73
  pending_sources = nil
68
74
  @source_cache.each_value do |source_by_batch_params|
69
75
  source_by_batch_params.each_value do |source|
@@ -77,10 +83,16 @@ module GraphQL
77
83
  if pending_sources
78
84
  fiber_vars = get_fiber_variables
79
85
  parent_task.async do
86
+ trace&.dataloader_spawn_source_fiber(pending_sources)
80
87
  set_fiber_variables(fiber_vars)
81
- Thread.current[:graphql_dataloader_next_tick] = condition
82
- pending_sources.each(&:run_pending_keys)
88
+ Fiber[:graphql_dataloader_next_tick] = condition
89
+ pending_sources.each do |s|
90
+ trace&.begin_dataloader_source(s)
91
+ s.run_pending_keys
92
+ trace&.end_dataloader_source(s)
93
+ end
83
94
  cleanup_fiber
95
+ trace&.dataloader_fiber_exit
84
96
  end
85
97
  end
86
98
  end
@@ -11,7 +11,7 @@ module GraphQL
11
11
  # executed synchronously.
12
12
  def run; end
13
13
  def run_isolated; yield; end
14
- def yield
14
+ def yield(_source)
15
15
  raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
16
16
  end
17
17
 
@@ -73,7 +73,7 @@ module GraphQL
73
73
  end
74
74
  }
75
75
 
76
- if pending_keys.any?
76
+ if !pending_keys.empty?
77
77
  sync(pending_keys)
78
78
  end
79
79
 
@@ -93,14 +93,14 @@ module GraphQL
93
93
  # Then run the batch and update the cache.
94
94
  # @return [void]
95
95
  def sync(pending_result_keys)
96
- @dataloader.yield
96
+ @dataloader.yield(self)
97
97
  iterations = 0
98
98
  while pending_result_keys.any? { |key| !@results.key?(key) }
99
99
  iterations += 1
100
100
  if iterations > MAX_ITERATIONS
101
101
  raise "#{self.class}#sync tried #{MAX_ITERATIONS} times to load pending keys (#{pending_result_keys}), but they still weren't loaded. There is likely a circular dependency#{@dataloader.fiber_limit ? " or `fiber_limit: #{@dataloader.fiber_limit}` is set too low" : ""}."
102
102
  end
103
- @dataloader.yield
103
+ @dataloader.yield(self)
104
104
  end
105
105
  nil
106
106
  end