graphql 2.5.9 → 2.5.26

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 (127) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/graphql/detailed_trace_generator.rb +77 -0
  3. data/lib/generators/graphql/templates/create_graphql_detailed_traces.erb +10 -0
  4. data/lib/graphql/analysis.rb +20 -13
  5. data/lib/graphql/dashboard/application_controller.rb +41 -0
  6. data/lib/graphql/dashboard/landings_controller.rb +9 -0
  7. data/lib/graphql/dashboard/statics_controller.rb +31 -0
  8. data/lib/graphql/dashboard/subscriptions.rb +2 -1
  9. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/_form.html.erb +1 -0
  10. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/edit.html.erb +2 -2
  11. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/index.html.erb +1 -1
  12. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/clients/new.html.erb +1 -1
  13. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/index_entries/index.html.erb +1 -1
  14. data/lib/graphql/dashboard/views/graphql/dashboard/operation_store/operations/show.html.erb +1 -1
  15. data/lib/graphql/dashboard/views/graphql/dashboard/subscriptions/topics/show.html.erb +1 -1
  16. data/lib/graphql/dashboard/views/layouts/graphql/dashboard/application.html.erb +7 -7
  17. data/lib/graphql/dashboard.rb +11 -73
  18. data/lib/graphql/dataloader/active_record_association_source.rb +14 -2
  19. data/lib/graphql/dataloader/async_dataloader.rb +22 -11
  20. data/lib/graphql/dataloader/null_dataloader.rb +54 -9
  21. data/lib/graphql/dataloader.rb +75 -23
  22. data/lib/graphql/date_encoding_error.rb +1 -1
  23. data/lib/graphql/execution/field_resolve_step.rb +631 -0
  24. data/lib/graphql/execution/finalize.rb +217 -0
  25. data/lib/graphql/execution/input_values.rb +261 -0
  26. data/lib/graphql/execution/interpreter/handles_raw_value.rb +6 -0
  27. data/lib/graphql/execution/interpreter/resolve.rb +10 -16
  28. data/lib/graphql/execution/interpreter/runtime/graphql_result.rb +13 -0
  29. data/lib/graphql/execution/interpreter/runtime.rb +28 -33
  30. data/lib/graphql/execution/interpreter.rb +8 -22
  31. data/lib/graphql/execution/lazy.rb +1 -1
  32. data/lib/graphql/execution/load_argument_step.rb +64 -0
  33. data/lib/graphql/execution/multiplex.rb +1 -1
  34. data/lib/graphql/execution/next.rb +90 -0
  35. data/lib/graphql/execution/prepare_object_step.rb +128 -0
  36. data/lib/graphql/execution/runner.rb +410 -0
  37. data/lib/graphql/execution/selections_step.rb +91 -0
  38. data/lib/graphql/execution.rb +8 -4
  39. data/lib/graphql/execution_error.rb +17 -10
  40. data/lib/graphql/introspection/directive_type.rb +7 -3
  41. data/lib/graphql/introspection/dynamic_fields.rb +5 -1
  42. data/lib/graphql/introspection/entry_points.rb +11 -3
  43. data/lib/graphql/introspection/enum_value_type.rb +5 -5
  44. data/lib/graphql/introspection/field_type.rb +13 -5
  45. data/lib/graphql/introspection/input_value_type.rb +21 -13
  46. data/lib/graphql/introspection/type_type.rb +64 -28
  47. data/lib/graphql/invalid_null_error.rb +11 -5
  48. data/lib/graphql/language/document_from_schema_definition.rb +2 -1
  49. data/lib/graphql/language/lexer.rb +20 -9
  50. data/lib/graphql/language/nodes.rb +5 -1
  51. data/lib/graphql/language/parser.rb +1 -0
  52. data/lib/graphql/language.rb +21 -12
  53. data/lib/graphql/pagination/connection.rb +2 -0
  54. data/lib/graphql/pagination/connections.rb +32 -0
  55. data/lib/graphql/query/context.rb +11 -4
  56. data/lib/graphql/query/null_context.rb +9 -3
  57. data/lib/graphql/query/partial.rb +18 -3
  58. data/lib/graphql/query.rb +10 -1
  59. data/lib/graphql/runtime_error.rb +6 -0
  60. data/lib/graphql/schema/addition.rb +3 -1
  61. data/lib/graphql/schema/argument.rb +17 -0
  62. data/lib/graphql/schema/build_from_definition.rb +15 -2
  63. data/lib/graphql/schema/directive.rb +45 -13
  64. data/lib/graphql/schema/field/connection_extension.rb +4 -37
  65. data/lib/graphql/schema/field/scope_extension.rb +18 -13
  66. data/lib/graphql/schema/field.rb +87 -48
  67. data/lib/graphql/schema/field_extension.rb +11 -8
  68. data/lib/graphql/schema/interface.rb +26 -0
  69. data/lib/graphql/schema/list.rb +5 -1
  70. data/lib/graphql/schema/member/base_dsl_methods.rb +1 -11
  71. data/lib/graphql/schema/member/has_arguments.rb +43 -14
  72. data/lib/graphql/schema/member/has_authorization.rb +35 -0
  73. data/lib/graphql/schema/member/has_dataloader.rb +37 -0
  74. data/lib/graphql/schema/member/has_fields.rb +86 -5
  75. data/lib/graphql/schema/member/has_interfaces.rb +2 -2
  76. data/lib/graphql/schema/member/type_system_helpers.rb +16 -2
  77. data/lib/graphql/schema/member.rb +5 -0
  78. data/lib/graphql/schema/non_null.rb +1 -1
  79. data/lib/graphql/schema/object.rb +1 -0
  80. data/lib/graphql/schema/ractor_shareable.rb +79 -0
  81. data/lib/graphql/schema/resolver.rb +60 -1
  82. data/lib/graphql/schema/subscription.rb +0 -2
  83. data/lib/graphql/schema/validator/required_validator.rb +45 -5
  84. data/lib/graphql/schema/visibility/migration.rb +2 -2
  85. data/lib/graphql/schema/visibility/profile.rb +140 -56
  86. data/lib/graphql/schema/visibility.rb +31 -18
  87. data/lib/graphql/schema/wrapper.rb +7 -1
  88. data/lib/graphql/schema.rb +108 -32
  89. data/lib/graphql/static_validation/all_rules.rb +1 -1
  90. data/lib/graphql/static_validation/base_visitor.rb +90 -66
  91. data/lib/graphql/static_validation/rules/argument_literals_are_compatible.rb +1 -1
  92. data/lib/graphql/static_validation/rules/argument_names_are_unique.rb +18 -6
  93. data/lib/graphql/static_validation/rules/arguments_are_defined.rb +5 -2
  94. data/lib/graphql/static_validation/rules/directives_are_defined.rb +5 -2
  95. data/lib/graphql/static_validation/rules/fields_are_defined_on_type.rb +4 -3
  96. data/lib/graphql/static_validation/rules/fields_have_appropriate_selections.rb +14 -4
  97. data/lib/graphql/static_validation/rules/fields_will_merge.rb +322 -256
  98. data/lib/graphql/static_validation/rules/fields_will_merge_error.rb +4 -4
  99. data/lib/graphql/static_validation/rules/fragment_spreads_are_possible.rb +3 -3
  100. data/lib/graphql/static_validation/rules/fragment_types_exist.rb +10 -7
  101. data/lib/graphql/static_validation/rules/required_arguments_are_present.rb +27 -7
  102. data/lib/graphql/static_validation/rules/unique_directives_per_location.rb +6 -2
  103. data/lib/graphql/static_validation/rules/variables_are_input_types.rb +12 -9
  104. data/lib/graphql/static_validation/validation_context.rb +1 -1
  105. data/lib/graphql/subscriptions/action_cable_subscriptions.rb +1 -0
  106. data/lib/graphql/subscriptions/default_subscription_resolve_extension.rb +34 -10
  107. data/lib/graphql/subscriptions/event.rb +1 -0
  108. data/lib/graphql/subscriptions.rb +36 -1
  109. data/lib/graphql/testing/helpers.rb +12 -9
  110. data/lib/graphql/testing/mock_action_cable.rb +111 -0
  111. data/lib/graphql/testing.rb +1 -0
  112. data/lib/graphql/tracing/detailed_trace/active_record_backend.rb +74 -0
  113. data/lib/graphql/tracing/detailed_trace.rb +70 -7
  114. data/lib/graphql/tracing/null_trace.rb +1 -1
  115. data/lib/graphql/tracing/perfetto_trace.rb +209 -79
  116. data/lib/graphql/tracing/sentry_trace.rb +3 -1
  117. data/lib/graphql/tracing/trace.rb +6 -0
  118. data/lib/graphql/type_kinds.rb +1 -0
  119. data/lib/graphql/types/relay/connection_behaviors.rb +8 -6
  120. data/lib/graphql/types/relay/edge_behaviors.rb +4 -3
  121. data/lib/graphql/types/relay/has_node_field.rb +13 -8
  122. data/lib/graphql/types/relay/has_nodes_field.rb +13 -8
  123. data/lib/graphql/types/relay/node_behaviors.rb +13 -2
  124. data/lib/graphql/unauthorized_error.rb +9 -1
  125. data/lib/graphql/version.rb +1 -1
  126. data/lib/graphql.rb +7 -3
  127. metadata +21 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d6def5d638ca63c24df116117624ac5bd6e87555457a38fb893a43a1b15600f
4
- data.tar.gz: c74e1ae2276871b07decc70422ff48992d8a5c9c9bdbce23164b367c707fe078
3
+ metadata.gz: 8b3c24503e050af2719344fdeb9305dbdd36100e3ae557ef217d618387f7b400
4
+ data.tar.gz: b20875732821e49db5ff5a1edec8f7e49ff98db65722932b9ab1e84c0f4a7212
5
5
  SHA512:
6
- metadata.gz: 333fdffd66163025d3dce807a12f7b2aeaceb932b5bd739d077a4d6d9ccbee92e891a24e68a8fb94f3f1157edc93abc01ac71fbef4ef0b64e2fc043ea4119488
7
- data.tar.gz: c22ba9de28b9124a1d758ffd9f71a7b49da20780c55bc9ceda9fdb74d93042df17eeccb8b74cf7eba9a095bd9852a19e0673c85e473da509379f4520cb695da7
6
+ metadata.gz: cfde5e5191cbec8091db9559b2f293c590dc7cc693c2d57e401c5e5c0da7c1e9a0bea745ee3f0e80e486ddb916ce65f5fee15d8c54fbeb2173b6900e22631b16
7
+ data.tar.gz: 9f2e0eb0c45670cc7e8c98414a09d5a203b431beff9d859af12215092ea770955eed9959e420f47e1c6a161e1ecdaf7a4d0a6fd9bd8d50ed16917cabe6d91920
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+ require 'rails/generators/active_record'
3
+
4
+ module Graphql
5
+ module Generators
6
+ class DetailedTraceGenerator < ::Rails::Generators::Base
7
+ include ::Rails::Generators::Migration
8
+ desc "Install GraphQL::Tracing::DetailedTrace for your schema"
9
+ source_root File.expand_path('../templates', __FILE__)
10
+
11
+ class_option :redis,
12
+ type: :boolean,
13
+ default: false,
14
+ desc: "Use Redis for persistence instead of ActiveRecord"
15
+
16
+ def self.next_migration_number(dirname)
17
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
18
+ end
19
+
20
+ def install_detailed_traces
21
+
22
+ schema_glob = File.expand_path("app/graphql/*_schema.rb", destination_root)
23
+ schema_file = Dir.glob(schema_glob).first
24
+ if !schema_file
25
+ raise ArgumentError, "Failed to find schema definition file (checked: #{schema_glob.inspect})"
26
+ end
27
+ schema_file_match = /( *)class ([A-Za-z:]+) < GraphQL::Schema/.match(File.read(schema_file))
28
+ schema_name = schema_file_match[2]
29
+ indent = schema_file_match[1] + " "
30
+
31
+ if !options.redis?
32
+ migration_template 'create_graphql_detailed_traces.erb', 'db/migrate/create_graphql_detailed_traces.rb'
33
+ end
34
+
35
+ log :add_detailed_traces_plugin
36
+ sentinel = /< GraphQL::Schema\s*\n/m
37
+ code = <<-RUBY
38
+ #{indent}use GraphQL::Tracing::DetailedTrace#{options.redis? ? ", redis: raise(\"TODO: pass a connection to a persistent redis database\")" : ""}, limit: 50
39
+
40
+ #{indent}# When this returns true, DetailedTrace will trace the query
41
+ #{indent}# Could use `query.context`, `query.selected_operation_name`, `query.query_string` here
42
+ #{indent}# Could call out to Flipper, etc
43
+ #{indent}def self.detailed_trace?(query)
44
+ #{indent} rand <= 0.000_1 # one in ten thousand
45
+ #{indent}end
46
+
47
+ RUBY
48
+
49
+ in_root do
50
+ inject_into_file schema_file, code, after: sentinel, force: false
51
+ end
52
+
53
+ routes_source = File.read(File.expand_path("config/routes.rb", destination_root))
54
+ already_has_dashboard = routes_source.include?("GraphQL::Dashboard") ||
55
+ routes_source.include?("Schema.dashboard") ||
56
+ routes_source.include?("GraphQL::Pro::Routes::Lazy")
57
+
58
+ if (!already_has_dashboard || behavior == :revoke)
59
+ log :route, "GraphQL::Dashboard"
60
+ shell.mute do
61
+ route <<~RUBY
62
+ # TODO: add authorization to this route and expose it in production
63
+ # See https://graphql-ruby.org/pro/dashboard.html#authorizing-the-dashboard
64
+ if Rails.env.development?
65
+ mount GraphQL::Dashboard, at: "/graphql/dashboard", schema: #{schema_name.inspect}
66
+ end
67
+
68
+ RUBY
69
+ end
70
+
71
+ gem("google-protobuf")
72
+
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,10 @@
1
+ class CreateGraphqlDetailedTraces < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :graphql_detailed_traces, force: true do |t|
4
+ t.bigint :begin_ms, null: false
5
+ t.float :duration_ms, null: false
6
+ t.binary :trace_data, null: false
7
+ t.string :operation_name, null: false
8
+ end
9
+ end
10
+ end
@@ -40,13 +40,13 @@ module GraphQL
40
40
  end
41
41
  end
42
42
 
43
- multiplex_results = multiplex_analyzers.map(&:result)
44
- multiplex_errors = analysis_errors(multiplex_results)
45
43
 
44
+ multiplex_analyzers.map!(&:result)
45
+ multiplex_errors = analysis_errors(EmptyObjects::EMPTY_ARRAY, multiplex_analyzers)
46
46
  multiplex.queries.each_with_index do |query, idx|
47
- query.analysis_errors = multiplex_errors + analysis_errors(query_results[idx])
47
+ query.analysis_errors = analysis_errors(multiplex_errors, query_results[idx])
48
48
  end
49
- multiplex_results
49
+ multiplex_analyzers
50
50
  end
51
51
  end
52
52
 
@@ -55,13 +55,11 @@ module GraphQL
55
55
  # @return [Array<Any>] Results from those analyzers
56
56
  def analyze_query(query, analyzers, multiplex_analyzers: [])
57
57
  query.current_trace.analyze_query(query: query) do
58
- query_analyzers = analyzers
59
- .map { |analyzer| analyzer.new(query) }
60
- .tap { _1.select!(&:analyze?) }
61
-
58
+ query_analyzers = analyzers.map { |analyzer| analyzer.new(query) }
59
+ query_analyzers.select!(&:analyze?)
62
60
  analyzers_to_run = query_analyzers + multiplex_analyzers
63
- if !analyzers_to_run.empty?
64
61
 
62
+ if !analyzers_to_run.empty?
65
63
  analyzers_to_run.select!(&:visit?)
66
64
  if !analyzers_to_run.empty?
67
65
  visitor = GraphQL::Analysis::Visitor.new(
@@ -79,18 +77,27 @@ module GraphQL
79
77
 
80
78
  query_analyzers.map(&:result)
81
79
  else
82
- []
80
+ EmptyObjects::EMPTY_ARRAY
83
81
  end
84
82
  end
85
83
  rescue TimeoutError => err
86
84
  [err]
87
85
  rescue GraphQL::UnauthorizedError, GraphQL::ExecutionError
88
86
  # This error was raised during analysis and will be returned the client before execution
89
- []
87
+ EmptyObjects::EMPTY_ARRAY
90
88
  end
91
89
 
92
- def analysis_errors(results)
93
- results.flatten.tap { _1.select! { |r| r.is_a?(GraphQL::AnalysisError) } }
90
+ def analysis_errors(parent_errors, results)
91
+ if !results.empty?
92
+ results = results.flatten
93
+ results.select! { |r| r.is_a?(GraphQL::AnalysisError) }
94
+ end
95
+
96
+ if parent_errors.empty?
97
+ results
98
+ else
99
+ parent_errors + results
100
+ end
94
101
  end
95
102
  end
96
103
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ require "action_controller"
3
+
4
+ module Graphql
5
+ class Dashboard < Rails::Engine
6
+ class ApplicationController < ActionController::Base
7
+ protect_from_forgery with: :exception
8
+ prepend_view_path(File.expand_path("../views", __FILE__))
9
+
10
+ content_security_policy do |policy|
11
+ policy.default_src(:self) if policy.default_src(*policy.default_src).blank?
12
+ policy.connect_src(:self) if policy.connect_src(*policy.connect_src).blank?
13
+ policy.base_uri(:none) if policy.base_uri(*policy.base_uri).blank?
14
+ policy.font_src(:self) if policy.font_src(*policy.font_src).blank?
15
+ policy.img_src(:self, :data) if policy.img_src(*policy.img_src).blank?
16
+ policy.object_src(:none) if policy.object_src(*policy.object_src).blank?
17
+ policy.script_src(:self) if policy.script_src(*policy.script_src).blank?
18
+ policy.style_src(:self) if policy.style_src(*policy.style_src).blank?
19
+ policy.form_action(:self) if policy.form_action(*policy.form_action).blank?
20
+ policy.frame_ancestors(:none) if policy.frame_ancestors(*policy.frame_ancestors).blank?
21
+ end
22
+
23
+ def schema_class
24
+ @schema_class ||= begin
25
+ schema_param = request.query_parameters["schema"] || params[:schema]
26
+ case schema_param
27
+ when Class
28
+ schema_param
29
+ when String
30
+ schema_param.constantize
31
+ else
32
+ raise "Missing `params[:schema]`, please provide a class or string to `mount GraphQL::Dashboard, schema: ...`"
33
+ end
34
+ end
35
+ end
36
+ helper_method :schema_class
37
+ end
38
+ end
39
+ end
40
+
41
+ ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module Graphql
3
+ class Dashboard < Rails::Engine
4
+ class LandingsController < ApplicationController
5
+ def show
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module Graphql
3
+ class Dashboard < Rails::Engine
4
+ class StaticsController < ApplicationController
5
+ skip_forgery_protection
6
+ # Use an explicit list of files to avoid any chance of reading other files from disk
7
+ STATICS = {}
8
+
9
+ [
10
+ "icon.png",
11
+ "header-icon.png",
12
+ "charts.min.css",
13
+ "dashboard.css",
14
+ "dashboard.js",
15
+ "bootstrap-5.3.3.min.css",
16
+ "bootstrap-5.3.3.min.js",
17
+ ].each do |static_file|
18
+ STATICS[static_file] = File.expand_path("../statics/#{static_file}", __FILE__)
19
+ end
20
+
21
+ def show
22
+ expires_in 1.year, public: true
23
+ if (filepath = STATICS[params[:id]])
24
+ render file: filepath
25
+ else
26
+ head :not_found
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require_relative "./installable"
2
3
  module Graphql
3
4
  class Dashboard < Rails::Engine
4
5
  module Subscriptions
@@ -6,7 +7,7 @@ module Graphql
6
7
  include Installable
7
8
 
8
9
  def feature_installed?
9
- schema_class.subscriptions.is_a?(GraphQL::Pro::Subscriptions)
10
+ defined?(GraphQL::Pro::Subscriptions) && schema_class.subscriptions.is_a?(GraphQL::Pro::Subscriptions)
10
11
  end
11
12
 
12
13
  INSTALLABLE_COMPONENT_HEADER_HTML = "GraphQL-Pro Subscriptions aren't installed on this schema yet.".html_safe
@@ -20,4 +20,5 @@
20
20
  <div class="col-auto">
21
21
  <%= link_to "Back", graphql_dashboard.operation_store_clients_path, class: "btn btn-outline-secondary" %>
22
22
  </div>
23
+ </div>
23
24
  <% end %>
@@ -3,10 +3,10 @@
3
3
  <div class="col">
4
4
  <h1>Edit <%= @client.name %></h1>
5
5
  </div>
6
- <div>
6
+ </div>
7
7
  <%= render partial: "graphql/dashboard/operation_store/clients/form" %>
8
8
 
9
- <hr class="mt-5"/>
9
+ <hr class="mt-5">
10
10
  <div class="row mt-5">
11
11
  <div class="col">
12
12
  <div class="alert alert-danger">
@@ -33,7 +33,7 @@
33
33
  <td><%= link_to(client.name, graphql_dashboard.edit_operation_store_client_path(name: client.name)) %></td>
34
34
  <td>
35
35
  <%= link_to(graphql_dashboard.operation_store_client_operations_path(client_name: client.name)) do %>
36
- <%= client.operations_count %><% if client.archived_operations_count > 0 %> <span class="muted">(<%=client.archived_operations_count%> archived)</span><% end %>
36
+ <%= client.operations_count %><% if client.archived_operations_count > 0 %> <span class="muted">(<%= client.archived_operations_count %> archived)</span><% end %>
37
37
  <% end %>
38
38
  </td>
39
39
  <td><%= client.created_at %></td>
@@ -3,5 +3,5 @@
3
3
  <div class="col">
4
4
  <h1>New Client</h1>
5
5
  </div>
6
- <div>
6
+ </div>
7
7
  <%= render partial: "graphql/dashboard/operation_store/clients/form" %>
@@ -10,7 +10,7 @@
10
10
  <form method="GET" action="<%= graphql_dashboard.operation_store_index_entries_path %>" style="margin-left: auto; margin-top:-5px;">
11
11
  <div class="input-group">
12
12
  <%= text_field_tag "q", @search_term, class: "form-control", placeholder: "Find types, fields, arguments, or enum values" %>
13
- <input type="submit" value="Search" class="btn btn-outline-primary btn-sm"/>
13
+ <input type="submit" value="Search" class="btn btn-outline-primary btn-sm">
14
14
  </div>
15
15
  </form>
16
16
  </div>
@@ -2,7 +2,7 @@
2
2
  <% if @operation.nil? %>
3
3
  <div class="row">
4
4
  <div class="col">
5
- <p>No stored operation found for <code><%= params[:digest] %></code>
5
+ <p>No stored operation found for <code><%= params[:digest] %></code></p>
6
6
  </div>
7
7
  </div>
8
8
  <% else %>
@@ -9,7 +9,7 @@
9
9
  <div class="col">
10
10
  <p>Last triggered: <%= @topic_last_triggered_at || "none" %></p>
11
11
  <p><%= pluralize(@subscriptions_count, "Subscription") %></p>
12
- <div>
12
+ </div>
13
13
  </div>
14
14
 
15
15
  <div class="row">
@@ -1,15 +1,15 @@
1
1
  <!doctype html>
2
2
  <html lang="en" class="h-100" >
3
3
  <head>
4
- <link rel="icon" type="image/png" href="<%= graphql_dashboard.static_path("icon.png") %>" />
4
+ <link rel="icon" type="image/png" href="<%= graphql_dashboard.static_path("icon.png") %>">
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
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") %>
8
+ <link rel="stylesheet" href="<%= graphql_dashboard.static_path("bootstrap-5.3.3.min.css") %>" media="screen">
9
+ <link rel="stylesheet" href="<%= graphql_dashboard.static_path("charts.min.css") %>" media="screen">
10
+ <link rel="stylesheet" href="<%= graphql_dashboard.static_path("dashboard.css") %>" media="screen">
11
+ <script src="<%= graphql_dashboard.static_path("bootstrap-5.3.3.min.js") %>"></script>
12
+ <script src="<%= graphql_dashboard.static_path("dashboard.js") %>"></script>
13
13
  <%= csrf_meta_tags %>
14
14
  </head>
15
15
  <body class="h-100 d-flex flex-column">
@@ -20,7 +20,7 @@
20
20
  <nav class="navbar navbar-expand-lg bg-body-tertiary">
21
21
  <div class="container-fluid">
22
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" />
23
+ <img id="header-icon" src="<%= graphql_dashboard.static_path("header-icon.png") %>" alt="GraphQL-Ruby">
24
24
  <% end %>
25
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
26
  <span class="navbar-toggler-icon"></span>
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  require 'rails/engine'
3
+ require 'action_controller'
3
4
  module Graphql
4
5
  # `GraphQL::Dashboard` is a `Rails::Engine`-based dashboard for viewing metadata about your GraphQL schema.
5
6
  #
@@ -34,7 +35,16 @@ module Graphql
34
35
  class Dashboard < Rails::Engine
35
36
  engine_name "graphql_dashboard"
36
37
  isolate_namespace(Graphql::Dashboard)
37
- routes.draw do
38
+
39
+ autoload :ApplicationController, "graphql/dashboard/application_controller"
40
+ autoload :LandingsController, "graphql/dashboard/landings_controller"
41
+ autoload :StaticsController, "graphql/dashboard/statics_controller"
42
+ autoload :DetailedTraces, "graphql/dashboard/detailed_traces"
43
+ autoload :Subscriptions, "graphql/dashboard/subscriptions"
44
+ autoload :OperationStore, "graphql/dashboard/operation_store"
45
+ autoload :Limiters, "graphql/dashboard/limiters"
46
+
47
+ routes do
38
48
  root "landings#show"
39
49
  resources :statics, only: :show, constraints: { id: /[0-9A-Za-z\-.]+/ }
40
50
 
@@ -77,82 +87,10 @@ module Graphql
77
87
  post "/subscriptions/clear_all", to: "subscriptions#clear_all", as: :clear_all
78
88
  end
79
89
  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
90
  end
146
91
  end
147
92
 
148
- require 'graphql/dashboard/detailed_traces'
149
- require 'graphql/dashboard/limiters'
150
- require 'graphql/dashboard/operation_store'
151
- require 'graphql/dashboard/subscriptions'
152
-
153
93
  # Rails expects the engine to be called `Graphql::Dashboard`,
154
94
  # but `GraphQL::Dashboard` is consistent with this gem's naming.
155
95
  # So define both constants to refer to the same class.
156
96
  GraphQL::Dashboard = Graphql::Dashboard
157
-
158
- ActiveSupport.run_load_hooks(:graphql_dashboard_application_controller, GraphQL::Dashboard::ApplicationController)
@@ -31,7 +31,12 @@ module GraphQL
31
31
  def fetch(records)
32
32
  record_classes = Set.new.compare_by_identity
33
33
  associated_classes = Set.new.compare_by_identity
34
+ scoped_fetch = !@scope.nil?
34
35
  records.each do |record|
36
+ if scoped_fetch
37
+ assoc = record.association(@association)
38
+ assoc.reset
39
+ end
35
40
  if record_classes.add?(record.class)
36
41
  reflection = record.class.reflect_on_association(@association)
37
42
  if !reflection.polymorphic? && reflection.klass
@@ -48,9 +53,16 @@ module GraphQL
48
53
 
49
54
  ::ActiveRecord::Associations::Preloader.new(records: records, associations: @association, available_records: available_records, scope: @scope).call
50
55
 
51
- loaded_associated_records = records.map { |r| r.public_send(@association) }
56
+ loaded_associated_records = records.map { |r|
57
+ assoc = r.association(@association)
58
+ lar = assoc.target
59
+ if scoped_fetch
60
+ assoc.reset
61
+ end
62
+ lar
63
+ }
52
64
 
53
- if @scope.nil?
65
+ if !scoped_fetch
54
66
  # Don't cache records loaded via scope because they might have reduced `SELECT`s
55
67
  # Could check .select_values here?
56
68
  records_by_model = {}
@@ -14,7 +14,7 @@ module GraphQL
14
14
  nil
15
15
  end
16
16
 
17
- def run
17
+ def run(trace_query_lazy: nil)
18
18
  trace = Fiber[:__graphql_current_multiplex]&.current_trace
19
19
  jobs_fiber_limit, total_fiber_limit = calculate_fiber_limit
20
20
  job_fibers = []
@@ -29,16 +29,7 @@ module GraphQL
29
29
  first_pass = false
30
30
  fiber_vars = get_fiber_variables
31
31
 
32
- while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
33
- if f.alive?
34
- finished = run_fiber(f)
35
- if !finished
36
- next_job_fibers << f
37
- end
38
- end
39
- end
40
- job_fibers.concat(next_job_fibers)
41
- next_job_fibers.clear
32
+ run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
42
33
 
43
34
  Sync do |root_task|
44
35
  set_fiber_variables(fiber_vars)
@@ -54,6 +45,13 @@ module GraphQL
54
45
  next_source_tasks.clear
55
46
  end
56
47
  end
48
+
49
+ if !@lazies_at_depth.empty?
50
+ with_trace_query_lazy(trace_query_lazy) do
51
+ run_next_pending_lazies(job_fibers, trace)
52
+ run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
53
+ end
54
+ end
57
55
  end
58
56
  trace&.end_dataloader(self)
59
57
  end
@@ -69,6 +67,19 @@ module GraphQL
69
67
 
70
68
  private
71
69
 
70
+ def run_pending_steps(job_fibers, next_job_fibers, source_tasks, jobs_fiber_limit, trace)
71
+ while (f = (job_fibers.shift || (((job_fibers.size + next_job_fibers.size + source_tasks.size) < jobs_fiber_limit) && spawn_job_fiber(trace))))
72
+ if f.alive?
73
+ finished = run_fiber(f)
74
+ if !finished
75
+ next_job_fibers << f
76
+ end
77
+ end
78
+ end
79
+ job_fibers.concat(next_job_fibers)
80
+ next_job_fibers.clear
81
+ end
82
+
72
83
  def spawn_source_task(parent_task, condition, trace)
73
84
  pending_sources = nil
74
85
  @source_cache.each_value do |source_by_batch_params|
@@ -2,23 +2,68 @@
2
2
 
3
3
  module GraphQL
4
4
  class Dataloader
5
- # The default implementation of dataloading -- all no-ops.
5
+ # GraphQL-Ruby uses this when Dataloader isn't enabled.
6
6
  #
7
- # The Dataloader interface isn't public, but it enables
8
- # simple internal code while adding the option to add Dataloader.
7
+ # It runs execution code inline and gathers lazy objects (eg. Promises)
8
+ # and resolves them during {#run}.
9
9
  class NullDataloader < Dataloader
10
- # These are all no-ops because code was
11
- # executed synchronously.
12
- def run; end
13
- def run_isolated; yield; end
10
+ def initialize(*)
11
+ @lazies_at_depth = Hash.new { |h,k| h[k] = [] }
12
+ end
13
+
14
+ def freeze
15
+ @lazies_at_depth.default_proc = nil
16
+ @lazies_at_depth.freeze
17
+ super
18
+ end
19
+
20
+ def run(trace_query_lazy: nil)
21
+ with_trace_query_lazy(trace_query_lazy) do
22
+ while !@lazies_at_depth.empty?
23
+ smallest_depth = nil
24
+ @lazies_at_depth.each_key do |depth_key|
25
+ smallest_depth ||= depth_key
26
+ if depth_key < smallest_depth
27
+ smallest_depth = depth_key
28
+ end
29
+ end
30
+
31
+ if smallest_depth
32
+ lazies = @lazies_at_depth.delete(smallest_depth)
33
+ lazies.each(&:value) # resolve these Lazy instances
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ def run_isolated
40
+ # Reuse this instance because execution code may already have a reference to _this_ `dataloader` inside the given block.
41
+ prev_lazies_at_depth = @lazies_at_depth
42
+ @lazies_at_depth = @lazies_at_depth.dup.clear
43
+ res = nil
44
+ append_job {
45
+ res = yield
46
+ }
47
+ run
48
+ res
49
+ ensure
50
+ @lazies_at_depth = prev_lazies_at_depth
51
+ end
52
+
53
+ def clear_cache; end
54
+
14
55
  def yield(_source)
15
56
  raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
16
57
  end
17
58
 
18
- def append_job
19
- yield
59
+ def append_job(callable = nil)
60
+ callable ? callable.call : yield
20
61
  nil
21
62
  end
63
+
64
+ def with(*)
65
+ raise GraphQL::Error, "GraphQL::Dataloader is not running -- add `use GraphQL::Dataloader` to your schema to use Dataloader sources."
66
+ end
22
67
  end
23
68
  end
24
69
  end