soql_dashboard 0.1.0.pre.test.2 → 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/Rakefile +1 -1
- data/app/controllers/soql_dashboard/reports_controller.rb +58 -19
- data/app/lib/soql_dashboard/salesforce_api_client.rb +60 -11
- 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 +2 -2
- data/app/views/soql_dashboard/reports/index.html.erb +53 -11
- 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/engine.rb +8 -0
- data/lib/soql_dashboard/version.rb +1 -1
- data/lib/soql_dashboard.rb +27 -2
- metadata +10 -6
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`:
|
data/Rakefile
CHANGED
|
@@ -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,16 +18,9 @@ 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
|
-
|
|
20
|
-
if @soql_query.present?
|
|
21
|
-
begin
|
|
22
|
-
@query_result = execute_soql_query(@soql_query)
|
|
23
|
-
rescue StandardError => e
|
|
24
|
-
@error = e.message
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
23
|
+
query_execution if @soql_query.present?
|
|
28
24
|
render :index
|
|
29
25
|
end
|
|
30
26
|
|
|
@@ -36,6 +32,26 @@ module SoqlDashboard
|
|
|
36
32
|
@integration = salesforce_integration_model.find_by(id: params[:integration_id])
|
|
37
33
|
end
|
|
38
34
|
|
|
35
|
+
def query_execution
|
|
36
|
+
service = SoqlDashboard::RunStatement.new
|
|
37
|
+
result = service.call(@soql_query, {
|
|
38
|
+
user: current_user_for_audit,
|
|
39
|
+
salesforce_integration: @integration,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if result[:error]
|
|
43
|
+
@error = result[:error]
|
|
44
|
+
else
|
|
45
|
+
@query_result = {
|
|
46
|
+
results: result[:result][:records] || [],
|
|
47
|
+
total_size: result[:result][:totalSize] || 0,
|
|
48
|
+
duration: result[:duration],
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
@error = e.message
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
def available_integrations
|
|
40
56
|
return [] unless salesforce_integration_model
|
|
41
57
|
|
|
@@ -54,21 +70,44 @@ module SoqlDashboard
|
|
|
54
70
|
end
|
|
55
71
|
|
|
56
72
|
def execute_soql_query(query)
|
|
57
|
-
|
|
73
|
+
raise "No integration selected" unless @integration
|
|
58
74
|
|
|
59
|
-
|
|
60
|
-
result =
|
|
75
|
+
service = SoqlDashboard::SoqlExecutor.new(@integration)
|
|
76
|
+
result = service.call(query)
|
|
61
77
|
|
|
78
|
+
raise result[:error] if result[:error]
|
|
79
|
+
|
|
80
|
+
{
|
|
81
|
+
query:,
|
|
82
|
+
results: result[:records] || [],
|
|
83
|
+
total_size: result[:totalSize] || 0,
|
|
84
|
+
}
|
|
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}" }
|
|
62
94
|
if result[:error]
|
|
63
|
-
|
|
95
|
+
Rails.logger.error "Failed to fetch objects: #{result[:error]}"
|
|
96
|
+
# Fallback to common Salesforce objects
|
|
97
|
+
FALLBACK_OBJECTS
|
|
64
98
|
else
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
results: result[:records] || [],
|
|
68
|
-
total_size: result[:totalSize] || 0,
|
|
69
|
-
done: result[:done] || true,
|
|
70
|
-
}
|
|
99
|
+
records = result[:records]&.pluck("QualifiedApiName")
|
|
100
|
+
records&.any? ? records : FALLBACK_OBJECTS
|
|
71
101
|
end
|
|
72
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
|
|
@@ -70,17 +80,56 @@ module SoqlDashboard
|
|
|
70
80
|
|
|
71
81
|
def parse_success_response(response)
|
|
72
82
|
result = JSON.parse(response.body)
|
|
83
|
+
|
|
73
84
|
{
|
|
74
|
-
|
|
75
|
-
done: result["done"],
|
|
85
|
+
total_size: result["totalSize"],
|
|
76
86
|
records: result["records"] || [],
|
|
77
87
|
}
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
def parse_bad_request_response(response)
|
|
81
91
|
error_data = JSON.parse(response.body)
|
|
82
|
-
error_message = error_data["message"] || "Bad Request"
|
|
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
|
|
@@ -7,7 +7,7 @@ module SoqlDashboard
|
|
|
7
7
|
@config = integration.send(config_method_name)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def call(query)
|
|
11
11
|
validate_query(query)
|
|
12
12
|
|
|
13
13
|
api_client = SoqlDashboard::SalesforceApiClient.new(config)
|
|
@@ -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
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
<div class="integrations-dropdown">
|
|
9
9
|
<%= form_with url: root_path, method: :get, local: true, id: "integration-selector-form" do |form| %>
|
|
10
10
|
<%= form.select :integration_id,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
options_for_select(
|
|
12
|
+
@integrations.map { |integration| ["#{integration[:name]} (ID: #{integration[:id]})", integration[:id]] },
|
|
13
|
+
@selected_integration&.id
|
|
14
|
+
),
|
|
15
|
+
{ prompt: "Select a Salesforce Integration..." },
|
|
16
|
+
{
|
|
17
|
+
class: "integration-dropdown",
|
|
18
|
+
onchange: "this.form.submit();"
|
|
19
|
+
}
|
|
20
|
+
%>
|
|
20
21
|
<% end %>
|
|
21
22
|
</div>
|
|
22
23
|
<% else %>
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
value: @soql_query,
|
|
38
39
|
placeholder: "SELECT Id, Name FROM Opportunity LIMIT 10",
|
|
39
40
|
rows: 5,
|
|
41
|
+
id: "soql_textarea",
|
|
40
42
|
class: "soql-textarea" %>
|
|
41
43
|
</div>
|
|
42
44
|
|
|
@@ -45,6 +47,28 @@
|
|
|
45
47
|
</div>
|
|
46
48
|
<% end %>
|
|
47
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 %>
|
|
48
72
|
|
|
49
73
|
<% if @error %>
|
|
50
74
|
<div class="error-section">
|
|
@@ -59,9 +83,9 @@
|
|
|
59
83
|
|
|
60
84
|
<div class="results-meta">
|
|
61
85
|
<p>
|
|
62
|
-
<strong>Query:</strong> <%= @
|
|
86
|
+
<strong>Query:</strong> <%= @soql_query %><br>
|
|
63
87
|
<strong>Total Records:</strong> <%= @query_result[:total_size] %><br>
|
|
64
|
-
<strong>
|
|
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
|
|
@@ -3,5 +3,13 @@
|
|
|
3
3
|
module SoqlDashboard
|
|
4
4
|
class Engine < ::Rails::Engine
|
|
5
5
|
isolate_namespace SoqlDashboard
|
|
6
|
+
|
|
7
|
+
initializer "soql_dashboard.assets" do |app|
|
|
8
|
+
if app.config.respond_to?(:assets) && defined?(Sprockets)
|
|
9
|
+
app.config.assets.precompile += [
|
|
10
|
+
"soql_dashboard/application.css",
|
|
11
|
+
]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
6
14
|
end
|
|
7
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
|
|
@@ -72,8 +76,8 @@ licenses:
|
|
|
72
76
|
- MIT
|
|
73
77
|
metadata:
|
|
74
78
|
homepage_uri: https://github.com/TrustedIQ/trustediq
|
|
75
|
-
source_code_uri: https://github.com/TrustedIQ/trustediq
|
|
76
|
-
changelog_uri: https://github.com/TrustedIQ/trustediq/
|
|
79
|
+
source_code_uri: https://github.com/TrustedIQ/trustediq/soql_dashboard
|
|
80
|
+
changelog_uri: https://github.com/TrustedIQ/trustediq/soql_dashboard
|
|
77
81
|
rubygems_mfa_required: 'true'
|
|
78
82
|
post_install_message:
|
|
79
83
|
rdoc_options: []
|
|
@@ -86,9 +90,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
86
90
|
version: 3.2.7
|
|
87
91
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
92
|
requirements:
|
|
89
|
-
- - "
|
|
93
|
+
- - ">="
|
|
90
94
|
- !ruby/object:Gem::Version
|
|
91
|
-
version:
|
|
95
|
+
version: '0'
|
|
92
96
|
requirements: []
|
|
93
97
|
rubygems_version: 3.4.19
|
|
94
98
|
signing_key:
|