soql_dashboard 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -0
- data/app/controllers/soql_dashboard/reports_controller.rb +41 -2
- data/app/lib/soql_dashboard/salesforce_api_client.rb +58 -9
- data/app/models/soql_dashboard/audit.rb +18 -0
- data/app/services/soql_dashboard/run_statement.rb +80 -0
- data/app/services/soql_dashboard/soql_executor.rb +1 -1
- data/app/views/soql_dashboard/reports/index.html.erb +43 -1
- data/db/migrate/20250919113356_create_soql_dashboard_audits.rb +16 -0
- data/lib/generators/soql_dashboard/install_generator.rb +15 -0
- data/lib/soql_dashboard/version.rb +1 -1
- data/lib/soql_dashboard.rb +27 -2
- metadata +6 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 0c11073ceb067d5a92c3edfe38f79307729b37fcb1680becb195f14ae7d029c6
         | 
| 4 | 
            +
              data.tar.gz: 1eb341095e2b0ceb594852e0caad8c837c988420119a08317dcf9db977243671
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: af39a082bfd872c230a86caa143929292a960e879e329989c70c30513fd31106e826048f2529a38406f70f49734548ed78fa4123ed090eacdce07214e25d0dbb
         | 
| 7 | 
            +
              data.tar.gz: b67f4ed627cca207f84aee102b754c18e818ee8a877dba812914eeaf695f815a7da004ba7bc4b3a34976ed37f2376df20b94566ab5f505f5634d9ae8904c8871
         | 
    
        data/README.md
    CHANGED
    
    | @@ -6,6 +6,7 @@ A Rails engine that provides a dashboard interface for querying Salesforce using | |
| 6 6 |  | 
| 7 7 | 
             
            - Query Salesforce objects using SOQL
         | 
| 8 8 | 
             
            - View results in a user-friendly dashboard
         | 
| 9 | 
            +
            - Audit trail for all SOQL queries with user tracking
         | 
| 9 10 | 
             
            - Easily mountable in any Rails application
         | 
| 10 11 |  | 
| 11 12 | 
             
            ## Installation
         | 
| @@ -20,8 +21,14 @@ Then run: | |
| 20 21 |  | 
| 21 22 | 
             
            ```sh
         | 
| 22 23 | 
             
            bundle install
         | 
| 24 | 
            +
            rails generate soql_dashboard:install
         | 
| 25 | 
            +
            rails soql_dashboard:install:migrations
         | 
| 26 | 
            +
            rails db:migrate
         | 
| 23 27 | 
             
            ```
         | 
| 24 28 |  | 
| 29 | 
            +
            This will create the necessary database table:
         | 
| 30 | 
            +
            - `soql_dashboard_audits` - tracks all query executions for audit purposes
         | 
| 31 | 
            +
             | 
| 25 32 | 
             
            ## Mounting the Engine
         | 
| 26 33 |  | 
| 27 34 | 
             
            In your application's `config/routes.rb`:
         | 
| @@ -4,9 +4,12 @@ module SoqlDashboard | |
| 4 4 | 
             
              class ReportsController < ApplicationController
         | 
| 5 5 | 
             
                before_action :set_integration, only: %i[index execute_query]
         | 
| 6 6 |  | 
| 7 | 
            +
                FALLBACK_OBJECTS = %w[Account Contact Opportunity Lead Case User].freeze
         | 
| 8 | 
            +
             | 
| 7 9 | 
             
                def index
         | 
| 8 10 | 
             
                  @integrations = available_integrations
         | 
| 9 11 | 
             
                  @selected_integration = @integration
         | 
| 12 | 
            +
                  @objects = integration_objects
         | 
| 10 13 | 
             
                  @query_result = nil
         | 
| 11 14 | 
             
                end
         | 
| 12 15 |  | 
| @@ -15,6 +18,7 @@ module SoqlDashboard | |
| 15 18 |  | 
| 16 19 | 
             
                  @integrations = available_integrations
         | 
| 17 20 | 
             
                  @selected_integration = @integration
         | 
| 21 | 
            +
                  @objects = integration_objects
         | 
| 18 22 | 
             
                  @soql_query = params[:soql_query]
         | 
| 19 23 | 
             
                  query_execution if @soql_query.present?
         | 
| 20 24 | 
             
                  render :index
         | 
| @@ -29,11 +33,20 @@ module SoqlDashboard | |
| 29 33 | 
             
                end
         | 
| 30 34 |  | 
| 31 35 | 
             
                def query_execution
         | 
| 32 | 
            -
                   | 
| 36 | 
            +
                  service = SoqlDashboard::RunStatement.new
         | 
| 37 | 
            +
                  result = service.call(@soql_query, {
         | 
| 38 | 
            +
                    user: current_user_for_audit,
         | 
| 39 | 
            +
                    salesforce_integration: @integration,
         | 
| 40 | 
            +
                  })
         | 
| 41 | 
            +
             | 
| 33 42 | 
             
                  if result[:error]
         | 
| 34 43 | 
             
                    @error = result[:error]
         | 
| 35 44 | 
             
                  else
         | 
| 36 | 
            -
                    @query_result =  | 
| 45 | 
            +
                    @query_result = {
         | 
| 46 | 
            +
                      results: result[:result][:records] || [],
         | 
| 47 | 
            +
                      total_size: result[:result][:totalSize] || 0,
         | 
| 48 | 
            +
                      duration: result[:duration],
         | 
| 49 | 
            +
                    }
         | 
| 37 50 | 
             
                  end
         | 
| 38 51 | 
             
                rescue StandardError => e
         | 
| 39 52 | 
             
                  @error = e.message
         | 
| @@ -70,5 +83,31 @@ module SoqlDashboard | |
| 70 83 | 
             
                    total_size: result[:totalSize] || 0,
         | 
| 71 84 | 
             
                  }
         | 
| 72 85 | 
             
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def integration_objects
         | 
| 88 | 
            +
                  return [] unless @integration
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  client = SoqlDashboard::SalesforceApiClient.new(@integration.send(config_method_name))
         | 
| 91 | 
            +
                  result = client.list_all_queryable_objects
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  Rails.logger.debug { "integration_objects: #{result.to_json}" }
         | 
| 94 | 
            +
                  if result[:error]
         | 
| 95 | 
            +
                    Rails.logger.error "Failed to fetch objects: #{result[:error]}"
         | 
| 96 | 
            +
                    # Fallback to common Salesforce objects
         | 
| 97 | 
            +
                    FALLBACK_OBJECTS
         | 
| 98 | 
            +
                  else
         | 
| 99 | 
            +
                    records = result[:records]&.pluck("QualifiedApiName")
         | 
| 100 | 
            +
                    records&.any? ? records : FALLBACK_OBJECTS
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def current_user_for_audit
         | 
| 105 | 
            +
                  return nil unless SoqlDashboard.user_class
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  method_name = SoqlDashboard.user_method
         | 
| 108 | 
            +
                  respond_to?(method_name, true) ? send(method_name) : nil
         | 
| 109 | 
            +
                rescue StandardError
         | 
| 110 | 
            +
                  nil
         | 
| 111 | 
            +
                end
         | 
| 73 112 | 
             
              end
         | 
| 74 113 | 
             
            end
         | 
| @@ -6,7 +6,7 @@ require "json" | |
| 6 6 | 
             
            require "timeout"
         | 
| 7 7 |  | 
| 8 8 | 
             
            module SoqlDashboard
         | 
| 9 | 
            -
              class SalesforceApiClient
         | 
| 9 | 
            +
              class SalesforceApiClient # rubocop:disable Metrics/ClassLength
         | 
| 10 10 | 
             
                def initialize(config)
         | 
| 11 11 | 
             
                  @config = config
         | 
| 12 12 | 
             
                end
         | 
| @@ -18,14 +18,24 @@ module SoqlDashboard | |
| 18 18 | 
             
                  response = http.request(request)
         | 
| 19 19 |  | 
| 20 20 | 
             
                  parse_response(response)
         | 
| 21 | 
            -
                rescue Timeout::Error
         | 
| 22 | 
            -
                  { error: "Request timed out. Please try again." }
         | 
| 23 | 
            -
                rescue Net::HTTPError => e
         | 
| 24 | 
            -
                  { error: "Network error: #{e.message}" }
         | 
| 25 | 
            -
                rescue JSON::ParserError
         | 
| 26 | 
            -
                  { error: "Invalid response from Salesforce API" }
         | 
| 27 21 | 
             
                rescue StandardError => e
         | 
| 28 | 
            -
                   | 
| 22 | 
            +
                  handle_error(e)
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def list_all_queryable_objects
         | 
| 26 | 
            +
                  # Use describe_global to get all available objects
         | 
| 27 | 
            +
                  describe_result = describe_global
         | 
| 28 | 
            +
                  return describe_result if describe_result[:error]
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  # Filter to only queryable objects
         | 
| 31 | 
            +
                  queryable_objects = describe_result[:sobjects]&.select do |sobject|
         | 
| 32 | 
            +
                    sobject["queryable"] == true
         | 
| 33 | 
            +
                  end&.pluck("name") || []
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  {
         | 
| 36 | 
            +
                    total_size: queryable_objects.length,
         | 
| 37 | 
            +
                    records: queryable_objects.map { |name| { "QualifiedApiName" => name } },
         | 
| 38 | 
            +
                  }
         | 
| 29 39 | 
             
                end
         | 
| 30 40 |  | 
| 31 41 | 
             
                private
         | 
| @@ -72,7 +82,7 @@ module SoqlDashboard | |
| 72 82 | 
             
                  result = JSON.parse(response.body)
         | 
| 73 83 |  | 
| 74 84 | 
             
                  {
         | 
| 75 | 
            -
                     | 
| 85 | 
            +
                    total_size: result["totalSize"],
         | 
| 76 86 | 
             
                    records: result["records"] || [],
         | 
| 77 87 | 
             
                  }
         | 
| 78 88 | 
             
                end
         | 
| @@ -82,5 +92,44 @@ module SoqlDashboard | |
| 82 92 | 
             
                  error_message = error_data.first["message"] || "Bad Request"
         | 
| 83 93 | 
             
                  { error: "SOQL Error: #{error_message}" }
         | 
| 84 94 | 
             
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def handle_error(error)
         | 
| 97 | 
            +
                  case error
         | 
| 98 | 
            +
                  when Timeout::Error
         | 
| 99 | 
            +
                    { error: "Request timed out. Please try again." }
         | 
| 100 | 
            +
                  when Net::HTTPError
         | 
| 101 | 
            +
                    { error: "Network error: #{error.message}" }
         | 
| 102 | 
            +
                  when JSON::ParserError
         | 
| 103 | 
            +
                    { error: "Invalid response from Salesforce API" }
         | 
| 104 | 
            +
                  else
         | 
| 105 | 
            +
                    { error: "Unexpected error: #{error.message}" }
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                def describe_global
         | 
| 110 | 
            +
                  uri = build_describe_uri
         | 
| 111 | 
            +
                  http = setup_http_connection(uri)
         | 
| 112 | 
            +
                  request = create_request(uri)
         | 
| 113 | 
            +
                  response = http.request(request)
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                  parse_describe_response(response)
         | 
| 116 | 
            +
                rescue StandardError => e
         | 
| 117 | 
            +
                  handle_error(e)
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def build_describe_uri
         | 
| 121 | 
            +
                  URI("#{config['instance_url']}/services/data/v58.0/sobjects/")
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                def parse_describe_response(response)
         | 
| 125 | 
            +
                  case response.code.to_i
         | 
| 126 | 
            +
                  when 200
         | 
| 127 | 
            +
                    result = JSON.parse(response.body)
         | 
| 128 | 
            +
                    { sobjects: result["sobjects"] || [] }
         | 
| 129 | 
            +
                  else
         | 
| 130 | 
            +
                    # Reuse the error handling from parse_response for common HTTP errors
         | 
| 131 | 
            +
                    parse_response(response)
         | 
| 132 | 
            +
                  end
         | 
| 133 | 
            +
                end
         | 
| 85 134 | 
             
              end
         | 
| 86 135 | 
             
            end
         | 
| @@ -0,0 +1,18 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SoqlDashboard
         | 
| 4 | 
            +
              class Audit < ApplicationRecord
         | 
| 5 | 
            +
                self.table_name = "soql_dashboard_audits"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                belongs_to :user, optional: true, class_name: SoqlDashboard.user_class.to_s if SoqlDashboard.user_class
         | 
| 8 | 
            +
                if SoqlDashboard.salesforce_integration_model
         | 
| 9 | 
            +
                  belongs_to :salesforce_integration, optional: true,
         | 
| 10 | 
            +
                    class_name: SoqlDashboard.salesforce_integration_model.to_s
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                validates :statement, presence: true
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                scope :recent, -> { order(created_at: :desc) }
         | 
| 16 | 
            +
                scope :for_integration, ->(integration_id) { where(salesforce_integration_id: integration_id) }
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
            end
         | 
| @@ -0,0 +1,80 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module SoqlDashboard
         | 
| 4 | 
            +
              class RunStatement
         | 
| 5 | 
            +
                def call(query_text, options = {})
         | 
| 6 | 
            +
                  audit = create_audit_record(query_text, options) if audit_enabled?
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  execution_result = execute_with_timing(query_text, options)
         | 
| 9 | 
            +
                  update_audit_record(audit, execution_result) if audit
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  build_response(execution_result)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                private
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                def audit_enabled?
         | 
| 17 | 
            +
                  SoqlDashboard.audit
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                def create_audit_record(query_text, options)
         | 
| 21 | 
            +
                  audit = SoqlDashboard::Audit.new(statement: query_text)
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  assign_audit_associations(audit, options)
         | 
| 24 | 
            +
                  audit.save!
         | 
| 25 | 
            +
                  audit
         | 
| 26 | 
            +
                end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                def assign_audit_associations(audit, options)
         | 
| 29 | 
            +
                  audit.user = options[:user] if options[:user] && audit.respond_to?(:user=)
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  return unless options[:salesforce_integration] && audit.respond_to?(:salesforce_integration=)
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  audit.salesforce_integration = options[:salesforce_integration]
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def execute_with_timing(query_text, options)
         | 
| 37 | 
            +
                  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  begin
         | 
| 40 | 
            +
                    result = execute_soql_query(query_text, options)
         | 
| 41 | 
            +
                    { success: true, result:, error: nil }
         | 
| 42 | 
            +
                  rescue StandardError => e
         | 
| 43 | 
            +
                    { success: false, result: nil, error: e.message }
         | 
| 44 | 
            +
                  ensure
         | 
| 45 | 
            +
                    duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
         | 
| 46 | 
            +
                    @duration = duration # Keep this for build_response method
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def execute_soql_query(query_text, options)
         | 
| 51 | 
            +
                  service = SoqlDashboard::SoqlExecutor.new(options[:salesforce_integration])
         | 
| 52 | 
            +
                  result = service.call(query_text)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  raise result[:error] if result[:error]
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  result
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                def update_audit_record(audit, execution_result)
         | 
| 60 | 
            +
                  return unless audit&.persisted?
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                  audit.duration = duration if audit.respond_to?(:duration=)
         | 
| 63 | 
            +
                  audit.error = execution_result[:error] if audit.respond_to?(:error=)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  audit.save! if audit.changed?
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def build_response(execution_result)
         | 
| 69 | 
            +
                  {
         | 
| 70 | 
            +
                    result: execution_result[:result],
         | 
| 71 | 
            +
                    error: execution_result[:error],
         | 
| 72 | 
            +
                    duration:,
         | 
| 73 | 
            +
                  }
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                def duration
         | 
| 77 | 
            +
                  @duration || 0.0
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
            end
         | 
| @@ -32,7 +32,7 @@ module SoqlDashboard | |
| 32 32 | 
             
                  query_lower = query.downcase
         | 
| 33 33 |  | 
| 34 34 | 
             
                  forbidden_keywords.each do |keyword|
         | 
| 35 | 
            -
                    raise ArgumentError, "#{keyword.upcase} statements are not allowed" if query_lower. | 
| 35 | 
            +
                    raise ArgumentError, "#{keyword.upcase} statements are not allowed" if query_lower.match?(/\b#{keyword}\b/)
         | 
| 36 36 | 
             
                  end
         | 
| 37 37 | 
             
                end
         | 
| 38 38 | 
             
              end
         | 
| @@ -38,6 +38,7 @@ | |
| 38 38 | 
             
                                         value: @soql_query,
         | 
| 39 39 | 
             
                                         placeholder: "SELECT Id, Name FROM Opportunity LIMIT 10",
         | 
| 40 40 | 
             
                                         rows: 5,
         | 
| 41 | 
            +
                                         id: "soql_textarea",
         | 
| 41 42 | 
             
                                         class: "soql-textarea" %>
         | 
| 42 43 | 
             
                    </div>
         | 
| 43 44 |  | 
| @@ -46,6 +47,28 @@ | |
| 46 47 | 
             
                    </div>
         | 
| 47 48 | 
             
                  <% end %>
         | 
| 48 49 | 
             
                </div>
         | 
| 50 | 
            +
                <% if @objects.is_a?(Array) && @objects.any? %>
         | 
| 51 | 
            +
                  <div class="field-selector" style="margin: 12px 0; padding: 12px; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 6px;">
         | 
| 52 | 
            +
                    <div style="display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap;">
         | 
| 53 | 
            +
                      <div>
         | 
| 54 | 
            +
                        <div style="font-weight: 600; margin-bottom: 6px; color: #374151;">
         | 
| 55 | 
            +
                          Available Salesforce objects
         | 
| 56 | 
            +
                        </div>
         | 
| 57 | 
            +
                        <select
         | 
| 58 | 
            +
                          id="available-objects"
         | 
| 59 | 
            +
                          style="min-width: 360px; width: 100%; max-width: 680px;"
         | 
| 60 | 
            +
                          class="integration-dropdown"
         | 
| 61 | 
            +
                          onchange="insertSelectedObject()"
         | 
| 62 | 
            +
                        >
         | 
| 63 | 
            +
                          <option disabled selected value="">Select an object...</option>
         | 
| 64 | 
            +
                          <% (@objects || []).sort.each do |obj_name| %>
         | 
| 65 | 
            +
                            <option value="<%= obj_name %>"><%= obj_name %></option>
         | 
| 66 | 
            +
                          <% end %>
         | 
| 67 | 
            +
                        </select>
         | 
| 68 | 
            +
                      </div>
         | 
| 69 | 
            +
                    </div>
         | 
| 70 | 
            +
                  </div>
         | 
| 71 | 
            +
                <% end %>
         | 
| 49 72 |  | 
| 50 73 | 
             
                <% if @error %>
         | 
| 51 74 | 
             
                  <div class="error-section">
         | 
| @@ -60,8 +83,9 @@ | |
| 60 83 |  | 
| 61 84 | 
             
                    <div class="results-meta">
         | 
| 62 85 | 
             
                      <p>
         | 
| 63 | 
            -
                        <strong>Query:</strong> <%= @ | 
| 86 | 
            +
                        <strong>Query:</strong> <%= @soql_query %><br>
         | 
| 64 87 | 
             
                        <strong>Total Records:</strong> <%= @query_result[:total_size] %><br>
         | 
| 88 | 
            +
                        <strong>Execution Time:</strong> <%= number_with_precision(@query_result[:duration], precision: 3) %>s<br>
         | 
| 65 89 | 
             
                      </p>
         | 
| 66 90 | 
             
                    </div>
         | 
| 67 91 |  | 
| @@ -97,3 +121,21 @@ | |
| 97 121 | 
             
                </div>
         | 
| 98 122 | 
             
              <% end %>
         | 
| 99 123 | 
             
            </div>
         | 
| 124 | 
            +
             | 
| 125 | 
            +
            <script>
         | 
| 126 | 
            +
              function insertSelectedObject() {
         | 
| 127 | 
            +
                try {
         | 
| 128 | 
            +
                  const select = document.getElementById('available-objects');
         | 
| 129 | 
            +
                  const value = select && select.value;
         | 
| 130 | 
            +
                  if(!value){ return; }
         | 
| 131 | 
            +
                  const textarea = document.getElementById('soql_textarea');
         | 
| 132 | 
            +
                  if(!textarea){ textarea = document.querySelector('textarea[name="soql_query"]'); }
         | 
| 133 | 
            +
                  const existing = (textarea && textarea.value) ? textarea.value : '';
         | 
| 134 | 
            +
                  const snippet = 'SELECT FIELDS(ALL) FROM ' + value + ' LIMIT 200';
         | 
| 135 | 
            +
                    textarea.value = snippet;
         | 
| 136 | 
            +
                  if (textarea) { textarea.focus(); }
         | 
| 137 | 
            +
                } catch (e) {
         | 
| 138 | 
            +
                  console.error('Failed to insert object', e);
         | 
| 139 | 
            +
                }
         | 
| 140 | 
            +
              }
         | 
| 141 | 
            +
            </script>
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class CreateSoqlDashboardAudits < ActiveRecord::Migration[7.0]
         | 
| 4 | 
            +
              def change
         | 
| 5 | 
            +
                create_table :soql_dashboard_audits do |t|
         | 
| 6 | 
            +
                  t.references :user, null: true, index: true
         | 
| 7 | 
            +
                  t.references :salesforce_integration, null: true, index: true
         | 
| 8 | 
            +
                  t.text :statement
         | 
| 9 | 
            +
                  t.float :duration
         | 
| 10 | 
            +
                  t.text :error
         | 
| 11 | 
            +
                  t.datetime :created_at
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                add_index :soql_dashboard_audits, :created_at
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "rails/generators/active_record"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module SoqlDashboard
         | 
| 6 | 
            +
              module Generators
         | 
| 7 | 
            +
                class InstallGenerator < Rails::Generators::Base
         | 
| 8 | 
            +
                  source_root File.join(__dir__, "install", "templates")
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def copy_config
         | 
| 11 | 
            +
                    template "soql_dashboard.rb", "config/initializers/soql_dashboard.rb"
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
            end
         | 
    
        data/lib/soql_dashboard.rb
    CHANGED
    
    | @@ -5,21 +5,46 @@ require "soql_dashboard/engine" | |
| 5 5 |  | 
| 6 6 | 
             
            module SoqlDashboard
         | 
| 7 7 | 
             
              class << self
         | 
| 8 | 
            -
                attr_accessor :configuration
         | 
| 8 | 
            +
                attr_accessor :configuration, :audit
         | 
| 9 9 | 
             
              end
         | 
| 10 10 |  | 
| 11 | 
            +
              self.audit = true
         | 
| 12 | 
            +
             | 
| 11 13 | 
             
              def self.configure
         | 
| 12 14 | 
             
                self.configuration ||= Configuration.new
         | 
| 13 15 | 
             
                yield(configuration) if block_given?
         | 
| 14 16 | 
             
              end
         | 
| 15 17 |  | 
| 18 | 
            +
              def self.user_class
         | 
| 19 | 
            +
                @user_class ||= configuration&.user_class || begin
         | 
| 20 | 
            +
                  User.name
         | 
| 21 | 
            +
                rescue StandardError
         | 
| 22 | 
            +
                  nil
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              def self.user_method
         | 
| 27 | 
            +
                @user_method ||= configuration&.user_method || (
         | 
| 28 | 
            +
                  if user_class
         | 
| 29 | 
            +
                    :"current_#{user_class.to_s.downcase.underscore}"
         | 
| 30 | 
            +
                  else
         | 
| 31 | 
            +
                    :current_user
         | 
| 32 | 
            +
                  end
         | 
| 33 | 
            +
                )
         | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              def self.salesforce_integration_model
         | 
| 37 | 
            +
                @salesforce_integration_model ||= configuration&.salesforce_integration_model
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 16 40 | 
             
              class Configuration
         | 
| 17 | 
            -
                attr_accessor :salesforce_integration_model, :user_class, :user_method
         | 
| 41 | 
            +
                attr_accessor :salesforce_integration_model, :user_class, :user_method, :audit
         | 
| 18 42 |  | 
| 19 43 | 
             
                def initialize
         | 
| 20 44 | 
             
                  @salesforce_integration_model = nil
         | 
| 21 45 | 
             
                  @user_class = nil
         | 
| 22 46 | 
             
                  @user_method = :current_user
         | 
| 47 | 
            +
                  @audit = true
         | 
| 23 48 | 
             
                end
         | 
| 24 49 | 
             
              end
         | 
| 25 50 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: soql_dashboard
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.2.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Fred Moura
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2025-09- | 
| 11 | 
            +
            date: 2025-09-22 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: pg
         | 
| @@ -58,11 +58,15 @@ files: | |
| 58 58 | 
             
            - app/lib/soql_dashboard/salesforce_api_client.rb
         | 
| 59 59 | 
             
            - app/mailers/soql_dashboard/application_mailer.rb
         | 
| 60 60 | 
             
            - app/models/soql_dashboard/application_record.rb
         | 
| 61 | 
            +
            - app/models/soql_dashboard/audit.rb
         | 
| 62 | 
            +
            - app/services/soql_dashboard/run_statement.rb
         | 
| 61 63 | 
             
            - app/services/soql_dashboard/soql_executor.rb
         | 
| 62 64 | 
             
            - app/views/layouts/soql_dashboard/application.html.erb
         | 
| 63 65 | 
             
            - app/views/soql_dashboard/reports/index.html.erb
         | 
| 64 66 | 
             
            - config/routes.rb
         | 
| 67 | 
            +
            - db/migrate/20250919113356_create_soql_dashboard_audits.rb
         | 
| 65 68 | 
             
            - lib/generators/soql_dashboard/install/templates/soql_dashboard.rb
         | 
| 69 | 
            +
            - lib/generators/soql_dashboard/install_generator.rb
         | 
| 66 70 | 
             
            - lib/soql_dashboard.rb
         | 
| 67 71 | 
             
            - lib/soql_dashboard/engine.rb
         | 
| 68 72 | 
             
            - lib/soql_dashboard/version.rb
         |