rails_spotlight 0.2.1 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41eb3756c5f0405d7811d039c0953ac3a62b993486a4fc2e2085686c8c9cd54b
4
- data.tar.gz: fd0d5d64ff53a660ade6867f05c152e36cd228ccb02b1d9006c3e4be8ed8e6cd
3
+ metadata.gz: 811024ca98d1eff1d90ec498134b41d547c19f0a731074f0f76b85f42d34ae62
4
+ data.tar.gz: fde226acfcae18f43435d64cccd3fae6b0651b0a606d3d196cd08bdaffa60403
5
5
  SHA512:
6
- metadata.gz: d3159584ad8395c2ea3a17fb969cd9a50c94a120c0db950ff11db8d52ce3357f443c7944a23dc8dc3521844f98e6cdc30da288f53a08e6308f8722c53a85907b
7
- data.tar.gz: 2057d0386f21959a19b229ca2f3b4e46db2a0270d1c8f1262f551363eeda54c7127bc3b78c1c209b6e949a62dc2ce1c82989b17650eaa7c6dc02bb99874f3389
6
+ metadata.gz: b4d436897c9ede10af86c7878abb46cccc119c0c2de60d68bab667ab88b58891108a875d259e64461dbcad36822ac6d5cb9793351a5ac63e88ee3aa7dd98cd52
7
+ data.tar.gz: ad6cea7c5217d8be6e9487aaa927ef9e90214a890d241000d89602ad9c1339186a90a5d1ee15578d9a09825e8e5e557e308243c3131170a4ea13ca6429892473
data/README.md CHANGED
@@ -41,8 +41,10 @@ file will be created in `config/rails_spotlight.yml`
41
41
  # Rest of the configuration is required for ActionCable. It will be disabled automatically in when ActionCable is not available.
42
42
  LIVE_CONSOLE_ENABLED: true
43
43
  REQUEST_COMPLETED_BROADCAST_ENABLED: false
44
- AUTO_MOUNT_ACTION_CABLE: true
44
+ AUTO_MOUNT_ACTION_CABLE: false
45
45
  ACTION_CABLE_MOUNT_PATH: /cable
46
+ BLOCK_EDITING_FILES: false
47
+ BLOCK_EDITING_FILES_OUTSIDE_OF_THE_PROJECT: true
46
48
  ```
47
49
 
48
50
  ## Troubleshooting
@@ -57,6 +59,14 @@ Solution:
57
59
  - Set AUTO_MOUNT_ACTION_CABLE: false
58
60
  - Add manually `mount ActionCable.server => '/cable'` to `config/routes.rb` with proper authentication method
59
61
 
62
+ ---
63
+
64
+ Requests crash when **ActionCable settings** -> **Use action cable for meta requests (required for Safari)** is on
65
+
66
+ Solution:
67
+ - Switch flag off
68
+ - REQUEST_COMPLETED_BROADCAST_ENABLED: false
69
+
60
70
  ## Testing
61
71
 
62
72
  To run tests for all versions of Rails and Ruby, run:
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../rails_command_executor'
3
4
  module RailsSpotlight
4
5
  module Channels
5
6
  class LiveConsoleChannel < ActionCable::Channel::Base
@@ -18,7 +19,7 @@ module RailsSpotlight
18
19
  for_project = data['project']
19
20
  return publish({ error: project_mismatch_message(for_project) }) if for_project.present? && for_project != project
20
21
 
21
- output = execute_command(command, { inspect_types: inspect_types })
22
+ output = execute_command(command, inspect_types)
22
23
  publish(output)
23
24
  end
24
25
 
@@ -37,54 +38,24 @@ module RailsSpotlight
37
38
  ::RailsSpotlight.config.project_name
38
39
  end
39
40
 
40
- def execute_command(command, opts = {})
41
- output_stream = StringIO.new # Create a new StringIO object to capture output
42
- inspect_types = opts[:inspect_types]
43
- result = nil
44
-
45
- begin
46
- original_stdout = $stdout
47
- $stdout = output_stream
48
- result = eval(command) # rubocop:disable Security/Eval
49
- ensure
50
- $stdout = original_stdout
51
- end
52
-
53
- # result = eval(command)
54
- {
55
- result: {
56
- inspect: result.inspect,
57
- raw: result,
58
- type: result.class.name,
59
- types: result_inspect_types(inspect_types, result),
60
- console: output_stream.string
61
- }
62
- }
63
- rescue StandardError => e
64
- { error: e.message }
41
+ def executor
42
+ @executor ||= ::RailsSpotlight::RailsCommandExecutor.new
65
43
  end
66
44
 
67
- def result_inspect_types(inspect_types, result)
68
- return {} unless inspect_types
45
+ def execute_command(command, inspect_types)
46
+ RailsSpotlight.config.logger && RailsSpotlight.config.logger.info("Executing command: #{command}") # rubocop:disable Style/SafeNavigation
69
47
 
70
- {
71
- root: result.class.name,
72
- items: result_types_items(result)
73
- }
74
- end
75
-
76
- def result_types_items(result)
77
- case result
78
- when Array
79
- # Create a hash with indices as keys and class names as values
80
- result.each_with_index.to_h { |element, index| [index.to_s, element.class.name] }
81
- when Hash
82
- # Create a hash with string keys and class names as values
83
- result.transform_keys(&:to_s).transform_values { |value| value.class.name }
48
+ executor.execute(command)
49
+ if executor.execution_successful?
50
+ {
51
+ result: executor.result_as_json(inspect_types: inspect_types),
52
+ project: ::RailsSpotlight.config.project_name
53
+ }
84
54
  else
85
- # For non-collection types, there are no items
86
- {}
55
+ executor.result_as_json
87
56
  end
57
+ rescue => e # rubocop:disable Style/RescueStandardError
58
+ { error: e.message }
88
59
  end
89
60
  end
90
61
  end
@@ -11,20 +11,23 @@ module RailsSpotlight
11
11
  }.freeze
12
12
 
13
13
  attr_reader :project_name, :source_path, :logger, :storage_path, :storage_pool_size, :middleware_skipped_paths,
14
- :not_encodable_event_values, :action_cable_mount_path
14
+ :not_encodable_event_values, :action_cable_mount_path,
15
+ :block_editing_files, :block_editing_files_outside_of_the_project
15
16
 
16
- def initialize(opts = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
17
+ def initialize(opts = {}) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
17
18
  @project_name = opts[:project_name] || detect_project_name
18
19
  @source_path = opts[:source_path] || self.class.rails_root
19
20
  @logger = opts[:logger] || Logger.new(File.join(self.class.rails_root, 'log', 'rails_spotlight.log'))
20
21
  @storage_path = opts[:storage_path] || File.join(self.class.rails_root, 'tmp', 'data', 'rails_spotlight')
21
22
  @storage_pool_size = opts[:storage_pool_size] || 20
22
- @live_console_enabled = opts[:live_console_enabled].nil? ? true : is_true?(opts[:live_console_enabled])
23
- @request_completed_broadcast_enabled = is_true?(opts[:request_completed_broadcast_enabled])
23
+ @live_console_enabled = opts[:live_console_enabled].nil? ? true : true?(opts[:live_console_enabled])
24
+ @request_completed_broadcast_enabled = true?(opts[:request_completed_broadcast_enabled])
24
25
  @middleware_skipped_paths = opts[:middleware_skipped_paths] || []
25
26
  @not_encodable_event_values = DEFAULT_NOT_ENCODABLE_EVENT_VALUES.merge(opts[:not_encodable_event_values] || {})
26
- @auto_mount_action_cable = opts[:auto_mount_action_cable].nil? ? true : is_true?(opts[:auto_mount_action_cable])
27
+ @auto_mount_action_cable = opts[:auto_mount_action_cable].nil? ? true : true?(opts[:auto_mount_action_cable])
27
28
  @action_cable_mount_path = opts[:action_cable_mount_path] || '/cable'
29
+ @block_editing_files = opts[:block_editing_files].nil? ? true : true?(opts[:block_editing_files])
30
+ @block_editing_files_outside_of_the_project = opts[:block_editing_files_outside_of_the_project].nil? ? false : true?(opts[:block_editing_files_outside_of_the_project])
28
31
  end
29
32
 
30
33
  def live_console_enabled
@@ -75,8 +78,8 @@ module RailsSpotlight
75
78
 
76
79
  private
77
80
 
78
- def is_true?(value)
79
- value == true || value == 'true' || value == 1 || value == '1'
81
+ def true?(value)
82
+ [true, 'true', 1, '1'].include?(value)
80
83
  end
81
84
 
82
85
  def detect_project_name
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../rails_command_executor'
4
+ module RailsSpotlight
5
+ module Middlewares
6
+ module Handlers
7
+ class ConsoleActionHandler < BaseActionHandler
8
+ def execute
9
+ validate_project!
10
+
11
+ RailsSpotlight.config.logger && RailsSpotlight.config.logger.info("Executing command: #{command}") # rubocop:disable Style/SafeNavigation
12
+ executor.execute(command)
13
+ end
14
+
15
+ private
16
+
17
+ def validate_project!
18
+ return if for_project.blank?
19
+ return if for_project.include?(::RailsSpotlight.config.project_name)
20
+
21
+ raise UnprocessableEntity, "Check your connection settings the current command is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project"
22
+ end
23
+
24
+ def executor
25
+ @executor ||= ::RailsSpotlight::RailsCommandExecutor.new
26
+ end
27
+
28
+ def inspect_types
29
+ @inspect_types ||= json_request_body.fetch('inspect_types')
30
+ end
31
+
32
+ def command
33
+ @command ||= json_request_body.fetch('command')
34
+ end
35
+
36
+ def for_project
37
+ @for_project ||= json_request_body['project']
38
+ end
39
+
40
+ def json_response_body
41
+ executor.result_as_json(inspect_types: inspect_types)
42
+ .merge(project: ::RailsSpotlight.config.project_name)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -7,7 +7,11 @@ module RailsSpotlight
7
7
  def execute
8
8
  raise NotFound, 'File not found' unless path_valid?
9
9
 
10
- File.write(file_path, new_content) if write_mode?
10
+ if write_mode?
11
+ try_to_update_file
12
+ else
13
+ File.read(file_path)
14
+ end
11
15
  rescue => e # rubocop:disable Style/RescueStandardError
12
16
  raise UnprocessableEntity, e.message
13
17
  end
@@ -34,6 +38,22 @@ module RailsSpotlight
34
38
  request_mode == 'write'
35
39
  end
36
40
 
41
+ def try_to_update_file
42
+ raise UnprocessableEntity, editing_files_block_msg if block_editing_files?
43
+ raise UnprocessableEntity, editing_files_blocked_err_msg if editing_outside_project_file_is_blocked?(file_path)
44
+
45
+ RailsSpotlight.config.logger && RailsSpotlight.config.logger.info("Updating file: #{file_path}") # rubocop:disable Style/SafeNavigation
46
+ File.write(file_path, new_content)
47
+ end
48
+
49
+ def editing_files_block_msg
50
+ 'Editing files is blocked. Please check the Rails spotlight BLOCK_EDITING_FILES configuration.'
51
+ end
52
+
53
+ def editing_files_blocked_err_msg
54
+ 'Editing files is blocked. Please check the Rails spotlight BLOCK_EDITING_FILES_OUTSIDE_OF_THE_PROJECT configuration.'
55
+ end
56
+
37
57
  def request_mode
38
58
  @request_mode ||= json_request_body.fetch('mode', 'read')
39
59
  end
@@ -43,12 +63,45 @@ module RailsSpotlight
43
63
  end
44
64
 
45
65
  def file_path
46
- @file_path ||= if json_request_body.fetch('file').start_with?(::RailsSpotlight.config.rails_root)
47
- json_request_body.fetch('file')
48
- else
49
- File.join(::RailsSpotlight.config.rails_root, json_request_body.fetch('file'))
66
+ @file_path ||= if path_file_in_project?
67
+ original_file_path
68
+ elsif file_in_project?
69
+ File.join(::RailsSpotlight.config.rails_root, original_file_path)
70
+ else # rubocop:disable Lint/DuplicateBranch
71
+ original_file_path
50
72
  end
51
73
  end
74
+
75
+ def original_file_path
76
+ @original_file_path ||= json_request_body.fetch('file')
77
+ end
78
+
79
+ def path_file_in_project?
80
+ @path_file_in_project ||= original_file_path.start_with?(::RailsSpotlight.config.rails_root)
81
+ end
82
+
83
+ def file_in_project?
84
+ File.exist?(File.join(::RailsSpotlight.config.rails_root, original_file_path))
85
+ end
86
+
87
+ def file_outside_project?
88
+ !file_in_project? && File.exist?(original_file_path)
89
+ end
90
+
91
+ def editing_outside_project_file_is_blocked?(file_path)
92
+ return false unless file_outside_project?
93
+ return false unless block_editing_files_outside_of_the_project?
94
+
95
+ !file_path.start_with?(::RailsSpotlight.config.rails_root)
96
+ end
97
+
98
+ def block_editing_files?
99
+ ::RailsSpotlight.config.block_editing_files
100
+ end
101
+
102
+ def block_editing_files_outside_of_the_project?
103
+ ::RailsSpotlight.config.block_editing_files_outside_of_the_project
104
+ end
52
105
  end
53
106
  end
54
107
  end
@@ -11,7 +11,8 @@ module RailsSpotlight
11
11
  def json_response_body
12
12
  {
13
13
  events: events,
14
- project: ::RailsSpotlight.config.project_name
14
+ project: ::RailsSpotlight.config.project_name,
15
+ root_path: ::RailsSpotlight.config.rails_root
15
16
  }
16
17
  end
17
18
 
@@ -16,11 +16,10 @@ module RailsSpotlight
16
16
  private
17
17
 
18
18
  def validate_project!
19
- Rails.logger.warn required_projects
20
19
  return if required_projects.blank?
21
20
  return if required_projects.include?(::RailsSpotlight.config.project_name)
22
21
 
23
- raise UnprocessableEntity, "Check your connetction settings the current query is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project"
22
+ raise UnprocessableEntity, "Check your connection settings the current query is not allowed to be executed on the #{::RailsSpotlight.config.project_name} project"
24
23
  end
25
24
 
26
25
  def transaction
@@ -37,7 +36,8 @@ module RailsSpotlight
37
36
  end
38
37
  end
39
38
 
40
- def run
39
+ def run # rubocop:disable Metrics/AbcSize
40
+ RailsSpotlight.config.logger && RailsSpotlight.config.logger.info("Executing query: #{query}") # rubocop:disable Style/SafeNavigation
41
41
  return self.result = ActiveRecord::Base.connection.exec_query(query) if connection_options.blank? || !ActiveRecord::Base.respond_to?(:connects_to)
42
42
 
43
43
  connections = ActiveRecord::Base.connects_to(**connection_options)
@@ -6,6 +6,7 @@ require_relative 'handlers/sql_action_handler'
6
6
  require_relative 'handlers/verify_action_handler'
7
7
  require_relative 'handlers/not_found_action_handler'
8
8
  require_relative 'handlers/meta_action_handler'
9
+ require_relative 'handlers/console_action_handler'
9
10
 
10
11
  module RailsSpotlight
11
12
  module Middlewares
@@ -33,6 +34,7 @@ module RailsSpotlight
33
34
  when 'sql' then Handlers::SqlActionHandler.new(*args).call
34
35
  when 'verify' then Handlers::VerifyActionHandler.new(*args).call
35
36
  when 'meta' then Handlers::MetaActionHandler.new(*args).call
37
+ when 'console' then Handlers::ConsoleActionHandler.new(*args).call
36
38
  else
37
39
  Handlers::NotFoundActionHandler.new(*args).call
38
40
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSpotlight
4
+ class RailsCommandExecutor
5
+ def execute(command)
6
+ output_stream = StringIO.new # Create a new StringIO object to capture output
7
+ @result = nil
8
+ @error = nil
9
+ @syntax_error = false
10
+
11
+ begin
12
+ original_stdout = $stdout
13
+ $stdout = output_stream
14
+ @result = eval(command) # rubocop:disable Security/Eval
15
+ rescue SyntaxError => e
16
+ @error = e
17
+ @syntax_error = true
18
+ rescue => e # rubocop:disable Style/RescueStandardError
19
+ @error = e
20
+ ensure
21
+ $stdout = original_stdout
22
+ end
23
+
24
+ @console = output_stream.string
25
+ self
26
+ rescue => e # rubocop:disable Style/RescueStandardError
27
+ @error = e
28
+ ensure
29
+ self
30
+ end
31
+
32
+ attr_reader :result, :console, :error, :syntax_error
33
+
34
+ def execution_successful?
35
+ error.nil?
36
+ end
37
+
38
+ def result_as_json(inspect_types: false)
39
+ if error
40
+ {
41
+ status: :error,
42
+ syntax_error: syntax_error,
43
+ error: error.respond_to?(:message) ? error.message : error.to_s,
44
+ backtrace: error.respond_to?(:backtrace) ? error.backtrace : nil
45
+ }
46
+ else
47
+ {
48
+ status: :ok,
49
+ inspect: result.inspect,
50
+ raw: result,
51
+ type: result.class.name,
52
+ types: result_inspect_types(inspect_types, result),
53
+ console: console
54
+ }
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def result_inspect_types(inspect_types, result)
61
+ return {} unless inspect_types
62
+
63
+ {
64
+ root: result.class.name,
65
+ items: result_types_items(result)
66
+ }
67
+ end
68
+
69
+ def result_types_items(result)
70
+ case result
71
+ when Array
72
+ # Create a hash with indices as keys and class names as values
73
+ result.each_with_index.to_h { |element, index| [index.to_s, element.class.name] }
74
+ when Hash
75
+ # Create a hash with string keys and class names as values
76
+ result.transform_keys(&:to_s).transform_values { |value| value.class.name }
77
+ else
78
+ # For non-collection types, there are no items
79
+ {}
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsSpotlight
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
data/lib/tasks/init.rake CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'rake'
4
4
 
5
- namespace :rails_spotlight do
5
+ namespace :rails_spotlight do # rubocop:disable Metrics/BlockLength
6
6
  desc 'Generate rails_spotlight configuration file'
7
7
  task generate_config: :environment do
8
8
  require 'fileutils'
@@ -21,8 +21,10 @@ namespace :rails_spotlight do
21
21
  # Rest of the configuration is required for ActionCable. It will be disabled automatically in when ActionCable is not available.
22
22
  LIVE_CONSOLE_ENABLED: true
23
23
  REQUEST_COMPLETED_BROADCAST_ENABLED: false
24
- AUTO_MOUNT_ACTION_CABLE: true
24
+ AUTO_MOUNT_ACTION_CABLE: false
25
25
  ACTION_CABLE_MOUNT_PATH: /cable
26
+ BLOCK_EDITING_FILES: false
27
+ BLOCK_EDITING_FILES_OUTSIDE_OF_THE_PROJECT: true
26
28
  YAML
27
29
 
28
30
  if File.exist?(config_path)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_spotlight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-11 00:00:00.000000000 Z
11
+ date: 2024-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack-contrib
@@ -248,6 +248,7 @@ files:
248
248
  - lib/rails_spotlight/middlewares.rb
249
249
  - lib/rails_spotlight/middlewares/concerns/skip_request_paths.rb
250
250
  - lib/rails_spotlight/middlewares/handlers/base_action_handler.rb
251
+ - lib/rails_spotlight/middlewares/handlers/console_action_handler.rb
251
252
  - lib/rails_spotlight/middlewares/handlers/file_action_handler.rb
252
253
  - lib/rails_spotlight/middlewares/handlers/meta_action_handler.rb
253
254
  - lib/rails_spotlight/middlewares/handlers/not_found_action_handler.rb
@@ -257,6 +258,7 @@ files:
257
258
  - lib/rails_spotlight/middlewares/main_request_handler.rb
258
259
  - lib/rails_spotlight/middlewares/request_completed.rb
259
260
  - lib/rails_spotlight/middlewares/request_handler.rb
261
+ - lib/rails_spotlight/rails_command_executor.rb
260
262
  - lib/rails_spotlight/railtie.rb
261
263
  - lib/rails_spotlight/storage.rb
262
264
  - lib/rails_spotlight/utils.rb