soql_dashboard 0.1.0.pre.test.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +58 -0
- data/Rakefile +10 -0
- data/app/assets/config/soql_dashboard_manifest.js +1 -0
- data/app/assets/stylesheets/soql_dashboard/application.css +276 -0
- data/app/controllers/soql_dashboard/application_controller.rb +32 -0
- data/app/controllers/soql_dashboard/reports_controller.rb +74 -0
- data/app/helpers/soql_dashboard/application_helper.rb +6 -0
- data/app/jobs/soql_dashboard/application_job.rb +6 -0
- data/app/lib/soql_dashboard/salesforce_api_client.rb +86 -0
- data/app/mailers/soql_dashboard/application_mailer.rb +8 -0
- data/app/models/soql_dashboard/application_record.rb +7 -0
- data/app/services/soql_dashboard/soql_executor.rb +39 -0
- data/app/views/layouts/soql_dashboard/application.html.erb +17 -0
- data/app/views/soql_dashboard/reports/index.html.erb +99 -0
- data/config/routes.rb +11 -0
- data/lib/generators/soql_dashboard/install/templates/soql_dashboard.rb +15 -0
- data/lib/soql_dashboard/engine.rb +7 -0
- data/lib/soql_dashboard/version.rb +5 -0
- data/lib/soql_dashboard.rb +25 -0
- data/lib/tasks/soql_dashboard_tasks.rake +6 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d081ecb90e2e5a9f2507e6f0347b734c5cdd8e5da27e0fe3efbe30ab4486f9d4
|
|
4
|
+
data.tar.gz: e6f8cebd896f7e8cdf854df8cc6be89e834a5bce48fa073ab55f2a54a0ed4434
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 70e996f4a9b28a828a4758bdbb9eca19ebaba92fd0d7041cf2b87134647cc3d792f34690a540e373c9f6a38f39f45224c7ff735589b43477283851faef914eca
|
|
7
|
+
data.tar.gz: ac851129e8ba96aed3fbd90ca69ab028fe1cec34ab1e1e1c64ffaaa374c30b035e289b017771fcaeb0f64e811cf4deeb0f5ae32bcb7f09f0ec114637d6dac227
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 TrustedIQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# SOQL Dashboard Engine
|
|
2
|
+
|
|
3
|
+
A Rails engine that provides a dashboard interface for querying Salesforce using SOQL.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Query Salesforce objects using SOQL
|
|
8
|
+
- View results in a user-friendly dashboard
|
|
9
|
+
- Easily mountable in any Rails application
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Add the engine to your application's Gemfile:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
gem 'soql_dashboard'
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then run:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bundle install
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Mounting the Engine
|
|
26
|
+
|
|
27
|
+
In your application's `config/routes.rb`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
mount SoqlDashboard::Engine => '/soql_dashboard'
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
- Visit `/soql_dashboard` in your browser to access the dashboard.
|
|
36
|
+
- Select a Salesforce model and run SOQL queries.
|
|
37
|
+
|
|
38
|
+
## Development
|
|
39
|
+
|
|
40
|
+
To work on the engine:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
cd engines/soql_dashboard
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run tests:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bundle exec rspec
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets/soql_dashboard .css
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
.soql-dashboard {
|
|
18
|
+
max-width: 1200px;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
padding: 20px;
|
|
21
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.soql-dashboard h1 {
|
|
25
|
+
color: #333;
|
|
26
|
+
border-bottom: 3px solid #0066cc;
|
|
27
|
+
padding-bottom: 10px;
|
|
28
|
+
margin-bottom: 30px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.integration-selector {
|
|
32
|
+
background: #f8f9fa;
|
|
33
|
+
padding: 20px;
|
|
34
|
+
border-radius: 8px;
|
|
35
|
+
margin-bottom: 30px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.integration-selector h3 {
|
|
39
|
+
margin-top: 0;
|
|
40
|
+
color: #495057;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.integrations-list {
|
|
44
|
+
display: grid;
|
|
45
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
46
|
+
gap: 15px;
|
|
47
|
+
margin-top: 15px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.integrations-dropdown {
|
|
51
|
+
margin-top: 15px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.integration-dropdown {
|
|
55
|
+
width: 100%;
|
|
56
|
+
max-width: 400px;
|
|
57
|
+
padding: 12px 16px;
|
|
58
|
+
font-size: 16px;
|
|
59
|
+
border: 2px solid #dee2e6;
|
|
60
|
+
border-radius: 6px;
|
|
61
|
+
background: white;
|
|
62
|
+
color: #333;
|
|
63
|
+
cursor: pointer;
|
|
64
|
+
transition: all 0.2s ease;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.integration-dropdown:hover {
|
|
68
|
+
border-color: #0066cc;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.integration-dropdown:focus {
|
|
72
|
+
outline: none;
|
|
73
|
+
border-color: #0066cc;
|
|
74
|
+
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.integration-item {
|
|
78
|
+
background: white;
|
|
79
|
+
border: 2px solid #dee2e6;
|
|
80
|
+
border-radius: 6px;
|
|
81
|
+
transition: all 0.2s ease;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.integration-item:hover {
|
|
85
|
+
border-color: #0066cc;
|
|
86
|
+
box-shadow: 0 2px 8px rgba(0, 102, 204, 0.1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.integration-item.selected {
|
|
90
|
+
border-color: #0066cc;
|
|
91
|
+
background: #e3f2fd;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.integration-link {
|
|
95
|
+
display: block;
|
|
96
|
+
padding: 15px;
|
|
97
|
+
text-decoration: none;
|
|
98
|
+
color: #333;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.integration-link:hover {
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
color: #333;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.integration-link strong {
|
|
107
|
+
display: block;
|
|
108
|
+
font-size: 16px;
|
|
109
|
+
margin-bottom: 5px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.integration-link small {
|
|
113
|
+
color: #6c757d;
|
|
114
|
+
font-size: 12px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.no-integrations {
|
|
118
|
+
color: #dc3545;
|
|
119
|
+
font-style: italic;
|
|
120
|
+
text-align: center;
|
|
121
|
+
padding: 20px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.query-section {
|
|
125
|
+
background: white;
|
|
126
|
+
border: 1px solid #dee2e6;
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
padding: 25px;
|
|
129
|
+
margin-bottom: 30px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.query-section h3 {
|
|
133
|
+
margin-top: 0;
|
|
134
|
+
color: #495057;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.query-input {
|
|
138
|
+
margin-bottom: 20px;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.query-input label {
|
|
142
|
+
display: block;
|
|
143
|
+
font-weight: 600;
|
|
144
|
+
margin-bottom: 8px;
|
|
145
|
+
color: #495057;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.soql-textarea {
|
|
149
|
+
width: 100%;
|
|
150
|
+
min-height: 120px;
|
|
151
|
+
padding: 12px;
|
|
152
|
+
border: 1px solid #ced4da;
|
|
153
|
+
border-radius: 4px;
|
|
154
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
line-height: 1.4;
|
|
157
|
+
resize: vertical;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.soql-textarea:focus {
|
|
161
|
+
outline: none;
|
|
162
|
+
border-color: #0066cc;
|
|
163
|
+
box-shadow: 0 0 0 2px rgba(0, 102, 204, 0.2);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.query-actions {
|
|
167
|
+
text-align: right;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.btn {
|
|
171
|
+
padding: 10px 20px;
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: 4px;
|
|
174
|
+
font-size: 14px;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
cursor: pointer;
|
|
177
|
+
text-decoration: none;
|
|
178
|
+
display: inline-block;
|
|
179
|
+
transition: all 0.2s ease;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.btn-primary {
|
|
183
|
+
background: #0066cc;
|
|
184
|
+
color: white;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.btn-primary:hover {
|
|
188
|
+
background: #0056b3;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.error-section {
|
|
192
|
+
background: #f8d7da;
|
|
193
|
+
border: 1px solid #f5c6cb;
|
|
194
|
+
border-radius: 8px;
|
|
195
|
+
padding: 20px;
|
|
196
|
+
margin-bottom: 30px;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.error-section h4 {
|
|
200
|
+
color: #721c24;
|
|
201
|
+
margin-top: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.error-message {
|
|
205
|
+
color: #721c24;
|
|
206
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
207
|
+
font-size: 14px;
|
|
208
|
+
background: rgba(114, 28, 36, 0.1);
|
|
209
|
+
padding: 10px;
|
|
210
|
+
border-radius: 4px;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.results-section {
|
|
214
|
+
background: white;
|
|
215
|
+
border: 1px solid #dee2e6;
|
|
216
|
+
border-radius: 8px;
|
|
217
|
+
padding: 25px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.results-section h4 {
|
|
221
|
+
margin-top: 0;
|
|
222
|
+
color: #495057;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.results-meta {
|
|
226
|
+
background: #f8f9fa;
|
|
227
|
+
padding: 15px;
|
|
228
|
+
border-radius: 4px;
|
|
229
|
+
margin-bottom: 20px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.results-meta p {
|
|
233
|
+
margin: 0;
|
|
234
|
+
font-size: 14px;
|
|
235
|
+
line-height: 1.6;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.results-table {
|
|
239
|
+
overflow-x: auto;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.results-table table {
|
|
243
|
+
width: 100%;
|
|
244
|
+
border-collapse: collapse;
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.results-table th,
|
|
249
|
+
.results-table td {
|
|
250
|
+
padding: 12px;
|
|
251
|
+
text-align: left;
|
|
252
|
+
border-bottom: 1px solid #dee2e6;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.results-table th {
|
|
256
|
+
background: #f8f9fa;
|
|
257
|
+
font-weight: 600;
|
|
258
|
+
color: #495057;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.results-table tr:hover {
|
|
262
|
+
background: #f8f9fa;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.no-selection {
|
|
266
|
+
text-align: center;
|
|
267
|
+
padding: 40px;
|
|
268
|
+
color: #6c757d;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.no-selection code {
|
|
272
|
+
background: #f8f9fa;
|
|
273
|
+
padding: 2px 6px;
|
|
274
|
+
border-radius: 3px;
|
|
275
|
+
font-size: 13px;
|
|
276
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SoqlDashboard
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
|
|
7
|
+
before_action :authenticate_user!
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def authenticate_user!
|
|
12
|
+
return if send(current_user_model).present?
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { redirect_to main_app.root_path, alert: "You must be logged in to access the SOQL Dashboard." }
|
|
16
|
+
format.json { render json: { error: "Authentication required" }, status: :unauthorized }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def current_user_model
|
|
21
|
+
return nil unless SoqlDashboard.configuration&.user_method
|
|
22
|
+
|
|
23
|
+
SoqlDashboard.configuration.user_method
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def salesforce_integration_model
|
|
27
|
+
return nil unless SoqlDashboard.configuration&.salesforce_integration_model
|
|
28
|
+
|
|
29
|
+
SoqlDashboard.configuration.salesforce_integration_model.constantize
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SoqlDashboard
|
|
4
|
+
class ReportsController < ApplicationController
|
|
5
|
+
before_action :set_integration, only: %i[index execute_query]
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@integrations = available_integrations
|
|
9
|
+
@selected_integration = @integration
|
|
10
|
+
@query_result = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def execute_query
|
|
14
|
+
return redirect_to root_path unless @integration
|
|
15
|
+
|
|
16
|
+
@integrations = available_integrations
|
|
17
|
+
@selected_integration = @integration
|
|
18
|
+
@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
|
+
|
|
28
|
+
render :index
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def set_integration
|
|
34
|
+
return unless params[:integration_id].present? && salesforce_integration_model
|
|
35
|
+
|
|
36
|
+
@integration = salesforce_integration_model.find_by(id: params[:integration_id])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def available_integrations
|
|
40
|
+
return [] unless salesforce_integration_model
|
|
41
|
+
|
|
42
|
+
salesforce_integration_model.all.map do |integration|
|
|
43
|
+
config = integration.send(config_method_name)
|
|
44
|
+
{
|
|
45
|
+
id: integration.id,
|
|
46
|
+
name: config["name"] || "Integration ##{integration.id}",
|
|
47
|
+
config:,
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def config_method_name
|
|
53
|
+
"#{SoqlDashboard::Engine.engine_name}_config"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def execute_soql_query(query)
|
|
57
|
+
return { error: "No integration selected" } unless @integration
|
|
58
|
+
|
|
59
|
+
executor = SoqlDashboard::SoqlExecutor.new(@integration)
|
|
60
|
+
result = executor.execute(query)
|
|
61
|
+
|
|
62
|
+
if result[:error]
|
|
63
|
+
{ error: result[:error] }
|
|
64
|
+
else
|
|
65
|
+
{
|
|
66
|
+
query:,
|
|
67
|
+
results: result[:records] || [],
|
|
68
|
+
total_size: result[:totalSize] || 0,
|
|
69
|
+
done: result[:done] || true,
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "timeout"
|
|
7
|
+
|
|
8
|
+
module SoqlDashboard
|
|
9
|
+
class SalesforceApiClient
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def execute_query(query)
|
|
15
|
+
uri = build_query_uri(query)
|
|
16
|
+
http = setup_http_connection(uri)
|
|
17
|
+
request = create_request(uri)
|
|
18
|
+
response = http.request(request)
|
|
19
|
+
|
|
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
|
+
rescue StandardError => e
|
|
28
|
+
{ error: "Unexpected error: #{e.message}" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :config
|
|
34
|
+
|
|
35
|
+
def build_query_uri(query)
|
|
36
|
+
escaped_query = URI.encode_www_form_component(query)
|
|
37
|
+
URI("#{config['instance_url']}/services/data/v58.0/query/?q=#{escaped_query}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def setup_http_connection(uri)
|
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
42
|
+
http.use_ssl = true
|
|
43
|
+
http.read_timeout = 30
|
|
44
|
+
http
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_request(uri)
|
|
48
|
+
request = Net::HTTP::Get.new(uri)
|
|
49
|
+
request["Authorization"] = "Bearer #{config['oauth_token']}"
|
|
50
|
+
request["Accept"] = "application/json"
|
|
51
|
+
request
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_response(response)
|
|
55
|
+
case response.code.to_i
|
|
56
|
+
when 200
|
|
57
|
+
parse_success_response(response)
|
|
58
|
+
when 400
|
|
59
|
+
parse_bad_request_response(response)
|
|
60
|
+
when 401
|
|
61
|
+
{ error: "Authentication failed. Please check your OAuth token." }
|
|
62
|
+
when 403
|
|
63
|
+
{ error: "Access denied. Insufficient permissions." }
|
|
64
|
+
when 404
|
|
65
|
+
{ error: "Salesforce API endpoint not found." }
|
|
66
|
+
else
|
|
67
|
+
{ error: "Salesforce API error: #{response.code} - #{response.message}" }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_success_response(response)
|
|
72
|
+
result = JSON.parse(response.body)
|
|
73
|
+
{
|
|
74
|
+
totalSize: result["totalSize"],
|
|
75
|
+
done: result["done"],
|
|
76
|
+
records: result["records"] || [],
|
|
77
|
+
}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def parse_bad_request_response(response)
|
|
81
|
+
error_data = JSON.parse(response.body)
|
|
82
|
+
error_message = error_data["message"] || "Bad Request"
|
|
83
|
+
{ error: "SOQL Error: #{error_message}" }
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SoqlDashboard
|
|
4
|
+
class SoqlExecutor
|
|
5
|
+
def initialize(integration)
|
|
6
|
+
@integration = integration
|
|
7
|
+
@config = integration.send(config_method_name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def execute(query)
|
|
11
|
+
validate_query(query)
|
|
12
|
+
|
|
13
|
+
api_client = SoqlDashboard::SalesforceApiClient.new(config)
|
|
14
|
+
api_client.execute_query(query)
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
{ error: e.message }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :integration, :config
|
|
22
|
+
|
|
23
|
+
def config_method_name
|
|
24
|
+
"#{SoqlDashboard::Engine.engine_name}_config"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_query(query)
|
|
28
|
+
raise ArgumentError, "Query cannot be blank" if query.blank?
|
|
29
|
+
raise ArgumentError, "Query must be a SELECT statement" unless query.strip.downcase.start_with?("select")
|
|
30
|
+
|
|
31
|
+
forbidden_keywords = %w[delete update insert upsert merge]
|
|
32
|
+
query_lower = query.downcase
|
|
33
|
+
|
|
34
|
+
forbidden_keywords.each do |keyword|
|
|
35
|
+
raise ArgumentError, "#{keyword.upcase} statements are not allowed" if query_lower.include?(keyword)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Soql dashboard</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "soql_dashboard/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<div class="soql-dashboard">
|
|
2
|
+
<h1>SOQL Dashboard</h1>
|
|
3
|
+
|
|
4
|
+
<div class="integration-selector">
|
|
5
|
+
<h3>Available Salesforce Integrations</h3>
|
|
6
|
+
|
|
7
|
+
<% if @integrations.any? %>
|
|
8
|
+
<div class="integrations-dropdown">
|
|
9
|
+
<%= form_with url: root_path, method: :get, local: true, id: "integration-selector-form" do |form| %>
|
|
10
|
+
<%= form.select :integration_id,
|
|
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
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<% else %>
|
|
23
|
+
<p class="no-integrations">No Salesforce integrations found. Please configure your integrations first.</p>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<% if @selected_integration %>
|
|
28
|
+
<div class="query-section">
|
|
29
|
+
<h3>SOQL Query for <%= @integrations.find { |i| i[:id] == @selected_integration.id }[:name] %></h3>
|
|
30
|
+
|
|
31
|
+
<%= form_with url: execute_query_reports_path, method: :post do |form| %>
|
|
32
|
+
<%= hidden_field_tag :integration_id, @selected_integration.id %>
|
|
33
|
+
|
|
34
|
+
<div class="query-input">
|
|
35
|
+
<%= form.label :soql_query, "Enter your SOQL query:" %>
|
|
36
|
+
<%= form.text_area :soql_query,
|
|
37
|
+
value: @soql_query,
|
|
38
|
+
placeholder: "SELECT Id, Name FROM Opportunity LIMIT 10",
|
|
39
|
+
rows: 5,
|
|
40
|
+
class: "soql-textarea" %>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="query-actions">
|
|
44
|
+
<%= form.submit "Execute Query", class: "btn btn-primary" %>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<% if @error %>
|
|
50
|
+
<div class="error-section">
|
|
51
|
+
<h4>Error</h4>
|
|
52
|
+
<div class="error-message"><%= @error %></div>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<% if @query_result %>
|
|
57
|
+
<div class="results-section">
|
|
58
|
+
<h4>Query Results</h4>
|
|
59
|
+
|
|
60
|
+
<div class="results-meta">
|
|
61
|
+
<p>
|
|
62
|
+
<strong>Query:</strong> <%= @query_result[:query] %><br>
|
|
63
|
+
<strong>Total Records:</strong> <%= @query_result[:total_size] %><br>
|
|
64
|
+
<strong>Status:</strong> <%= @query_result[:done] ? 'Complete' : 'Partial' %>
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<% if @query_result[:results]&.any? %>
|
|
69
|
+
<div class="results-table">
|
|
70
|
+
<table>
|
|
71
|
+
<thead>
|
|
72
|
+
<tr>
|
|
73
|
+
<% @query_result[:results].first.keys.reject { |k| k == "attributes" }.each do |column| %>
|
|
74
|
+
<th><%= column %></th>
|
|
75
|
+
<% end %>
|
|
76
|
+
</tr>
|
|
77
|
+
</thead>
|
|
78
|
+
<tbody>
|
|
79
|
+
<% @query_result[:results].each do |record| %>
|
|
80
|
+
<tr>
|
|
81
|
+
<% record.reject { |k, v| k == "attributes" }.values.each do |value| %>
|
|
82
|
+
<td><%= value %></td>
|
|
83
|
+
<% end %>
|
|
84
|
+
</tr>
|
|
85
|
+
<% end %>
|
|
86
|
+
</tbody>
|
|
87
|
+
</table>
|
|
88
|
+
</div>
|
|
89
|
+
<% else %>
|
|
90
|
+
<p>No results found.</p>
|
|
91
|
+
<% end %>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% else %>
|
|
95
|
+
<div class="no-selection">
|
|
96
|
+
<p>Please select a Salesforce integration above to start querying, or add <code>?integration_id=X</code> to the URL.</p>
|
|
97
|
+
</div>
|
|
98
|
+
<% end %>
|
|
99
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SoqlDashboard.configure do |config|
|
|
4
|
+
# The model class that represents Salesforce integrations
|
|
5
|
+
# Example: config.salesforce_integration_model = 'Integrations::SalesforceIntegration'
|
|
6
|
+
config.salesforce_integration_model = "CHANGE_ME"
|
|
7
|
+
|
|
8
|
+
# The user class for authentication
|
|
9
|
+
# Example: config.user_class = 'AdminUser'
|
|
10
|
+
config.user_class = "CHANGE_ME"
|
|
11
|
+
|
|
12
|
+
# The method to get the current user (should be available in controllers)
|
|
13
|
+
# Example: config.user_method = :current_admin_user
|
|
14
|
+
config.user_method = :CHANGE_ME
|
|
15
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "soql_dashboard/version"
|
|
4
|
+
require "soql_dashboard/engine"
|
|
5
|
+
|
|
6
|
+
module SoqlDashboard
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :configuration
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.configure
|
|
12
|
+
self.configuration ||= Configuration.new
|
|
13
|
+
yield(configuration) if block_given?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Configuration
|
|
17
|
+
attr_accessor :salesforce_integration_model, :user_class, :user_method
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@salesforce_integration_model = nil
|
|
21
|
+
@user_class = nil
|
|
22
|
+
@user_method = :current_user
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: soql_dashboard
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.test.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Fred Moura
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-09-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: pg
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.6'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rails
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 7.2.2.2
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 7.2.2.2
|
|
41
|
+
description: A Rails engine providing a dashboard interface for querying Salesforce
|
|
42
|
+
using SOQL
|
|
43
|
+
email:
|
|
44
|
+
- fred@trustediq.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- MIT-LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- Rakefile
|
|
52
|
+
- app/assets/config/soql_dashboard_manifest.js
|
|
53
|
+
- app/assets/stylesheets/soql_dashboard/application.css
|
|
54
|
+
- app/controllers/soql_dashboard/application_controller.rb
|
|
55
|
+
- app/controllers/soql_dashboard/reports_controller.rb
|
|
56
|
+
- app/helpers/soql_dashboard/application_helper.rb
|
|
57
|
+
- app/jobs/soql_dashboard/application_job.rb
|
|
58
|
+
- app/lib/soql_dashboard/salesforce_api_client.rb
|
|
59
|
+
- app/mailers/soql_dashboard/application_mailer.rb
|
|
60
|
+
- app/models/soql_dashboard/application_record.rb
|
|
61
|
+
- app/services/soql_dashboard/soql_executor.rb
|
|
62
|
+
- app/views/layouts/soql_dashboard/application.html.erb
|
|
63
|
+
- app/views/soql_dashboard/reports/index.html.erb
|
|
64
|
+
- config/routes.rb
|
|
65
|
+
- lib/generators/soql_dashboard/install/templates/soql_dashboard.rb
|
|
66
|
+
- lib/soql_dashboard.rb
|
|
67
|
+
- lib/soql_dashboard/engine.rb
|
|
68
|
+
- lib/soql_dashboard/version.rb
|
|
69
|
+
- lib/tasks/soql_dashboard_tasks.rake
|
|
70
|
+
homepage: https://github.com/TrustedIQ/trustediq
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/TrustedIQ/trustediq
|
|
75
|
+
source_code_uri: https://github.com/TrustedIQ/trustediq
|
|
76
|
+
changelog_uri: https://github.com/TrustedIQ/trustediq/blob/develop/CHANGELOG.md
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
post_install_message:
|
|
79
|
+
rdoc_options: []
|
|
80
|
+
require_paths:
|
|
81
|
+
- lib
|
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - ">="
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.2.7
|
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
88
|
+
requirements:
|
|
89
|
+
- - ">"
|
|
90
|
+
- !ruby/object:Gem::Version
|
|
91
|
+
version: 1.3.1
|
|
92
|
+
requirements: []
|
|
93
|
+
rubygems_version: 3.4.19
|
|
94
|
+
signing_key:
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: SOQL Dashboard for TrustedIQ Salesforce integrations
|
|
97
|
+
test_files: []
|