query_console 0.1.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 +7 -0
- data/MIT-LICENSE +21 -0
- data/README.md +382 -0
- data/Rakefile +11 -0
- data/app/controllers/query_console/application_controller.rb +38 -0
- data/app/controllers/query_console/queries_controller.rb +43 -0
- data/app/javascript/query_console/application.js +20 -0
- data/app/javascript/query_console/controllers/collapsible_controller.js +42 -0
- data/app/javascript/query_console/controllers/editor_controller.js +77 -0
- data/app/javascript/query_console/controllers/history_controller.js +124 -0
- data/app/services/query_console/audit_logger.rb +50 -0
- data/app/services/query_console/runner.rb +89 -0
- data/app/services/query_console/sql_limiter.rb +48 -0
- data/app/services/query_console/sql_validator.rb +72 -0
- data/app/views/query_console/queries/_results.html.erb +191 -0
- data/app/views/query_console/queries/new.html.erb +565 -0
- data/config/importmap.rb +13 -0
- data/config/routes.rb +4 -0
- data/lib/generators/query_console/install/README +28 -0
- data/lib/generators/query_console/install/install_generator.rb +19 -0
- data/lib/generators/query_console/install/templates/query_console.rb +61 -0
- data/lib/query_console/configuration.rb +41 -0
- data/lib/query_console/engine.rb +29 -0
- data/lib/query_console/version.rb +3 -0
- data/lib/query_console.rb +7 -0
- metadata +159 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5f57530c0dcf15456265758600392d0eeff17ba08cfff9f879d0afa31a1ab7e3
|
|
4
|
+
data.tar.gz: 12952c2bac1d22594a51e06d7c44e892b8e9955381091c22eb696ce08f7a8668
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c2c2c54e0c044763180321438121c11b66a5cabd8ebc6a1afe53eea8f58b22675424647b3c5b2bde568aeac62e4de5bfa2dcc2f1f32606f5a1a50eeb3960519f
|
|
7
|
+
data.tar.gz: 28d136f7b2c95ae7aeb551c07bf38bc8a861ff434885a75cbf805773935d4b3df8df57f2a165f9aca7e5ee23169822e635cd23b884d998626a1a7d654c012b57
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johnson Gnanasekar
|
|
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,382 @@
|
|
|
1
|
+
# QueryConsole
|
|
2
|
+
|
|
3
|
+
A Rails engine that provides a secure, mountable web interface for running read-only SQL queries against your application's database.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔒 **Security First**: Read-only queries enforced at multiple levels
|
|
8
|
+
- 🚦 **Environment Gating**: Disabled by default in production
|
|
9
|
+
- 🔑 **Flexible Authorization**: Integrate with your existing auth system
|
|
10
|
+
- 📊 **Modern UI**: Clean, responsive interface with query history
|
|
11
|
+
- 📝 **Audit Logging**: All queries logged with actor information
|
|
12
|
+
- ⚡ **Resource Protection**: Configurable row limits and query timeouts
|
|
13
|
+
- 💾 **Client-Side History**: Query history stored in browser localStorage
|
|
14
|
+
- ⚡ **Hotwire-Powered**: Uses Turbo Frames and Stimulus for smooth, SPA-like experience
|
|
15
|
+
- 🎨 **Zero Build Step**: CDN-hosted Hotwire, no asset compilation needed
|
|
16
|
+
|
|
17
|
+
## Security Features
|
|
18
|
+
|
|
19
|
+
QueryConsole implements multiple layers of security:
|
|
20
|
+
|
|
21
|
+
1. **Environment Gating**: Only enabled in configured environments (development by default)
|
|
22
|
+
2. **Authorization Hook**: Requires explicit authorization configuration
|
|
23
|
+
3. **SQL Validation**: Only SELECT and WITH (CTE) queries allowed
|
|
24
|
+
4. **Keyword Blocking**: Blocks all write operations (UPDATE, DELETE, INSERT, DROP, etc.)
|
|
25
|
+
5. **Statement Isolation**: Prevents multiple statement execution
|
|
26
|
+
6. **Row Limiting**: Automatic result limiting to prevent resource exhaustion
|
|
27
|
+
7. **Query Timeout**: Configurable timeout to prevent long-running queries
|
|
28
|
+
8. **Audit Trail**: All queries logged with structured data
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
Add this line to your application's Gemfile:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
gem 'query_console'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
And then execute:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
bundle install
|
|
42
|
+
rails generate query_console:install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Requirements:**
|
|
46
|
+
- Ruby 3.1+
|
|
47
|
+
- Rails 7.0+
|
|
48
|
+
- Works with Rails 8+
|
|
49
|
+
- Hotwire (Turbo Rails + Stimulus) - automatically included
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
The generator creates `config/initializers/query_console.rb`. You **MUST** configure the authorization hook:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
QueryConsole.configure do |config|
|
|
57
|
+
# Required: Set up authorization
|
|
58
|
+
config.authorize = ->(controller) {
|
|
59
|
+
controller.current_user&.admin?
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Track who runs queries
|
|
63
|
+
config.current_actor = ->(controller) {
|
|
64
|
+
controller.current_user&.email || "anonymous"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# Optional: Enable in additional environments (use with caution!)
|
|
68
|
+
# config.enabled_environments = %w[development staging]
|
|
69
|
+
|
|
70
|
+
# Optional: Adjust limits
|
|
71
|
+
# config.max_rows = 1000
|
|
72
|
+
# config.timeout_ms = 5000
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Authorization Examples
|
|
77
|
+
|
|
78
|
+
#### With Devise
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
config.authorize = ->(controller) {
|
|
82
|
+
controller.current_user&.admin?
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### With HTTP Basic Auth
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
config.authorize = ->(controller) {
|
|
90
|
+
controller.authenticate_or_request_with_http_basic do |username, password|
|
|
91
|
+
username == "admin" && password == Rails.application.credentials.query_console_password
|
|
92
|
+
end
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### For Development (NOT for production!)
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
config.authorize = ->(_controller) { true }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Configuration Options
|
|
103
|
+
|
|
104
|
+
| Option | Default | Description |
|
|
105
|
+
|--------|---------|-------------|
|
|
106
|
+
| `enabled_environments` | `["development"]` | Environments where console is accessible |
|
|
107
|
+
| `authorize` | `nil` | Lambda/proc that receives controller and returns true/false |
|
|
108
|
+
| `current_actor` | `->(_) { "unknown" }` | Lambda/proc to identify who's running queries |
|
|
109
|
+
| `max_rows` | `500` | Maximum rows returned per query |
|
|
110
|
+
| `timeout_ms` | `3000` | Query timeout in milliseconds |
|
|
111
|
+
| `forbidden_keywords` | See code | SQL keywords that are blocked |
|
|
112
|
+
| `allowed_starts_with` | `["select", "with"]` | Allowed query starting keywords |
|
|
113
|
+
|
|
114
|
+
## Mounting
|
|
115
|
+
|
|
116
|
+
Add to your `config/routes.rb`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
Rails.application.routes.draw do
|
|
120
|
+
mount QueryConsole::Engine, at: "/query_console"
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Then visit: `http://localhost:3000/query_console`
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
### Running Queries
|
|
129
|
+
|
|
130
|
+
1. Enter your SELECT query in the editor
|
|
131
|
+
2. Click "Run Query" or press Ctrl/Cmd+Enter
|
|
132
|
+
3. View results in the table below
|
|
133
|
+
4. Query is automatically saved to history
|
|
134
|
+
|
|
135
|
+
### Query History
|
|
136
|
+
|
|
137
|
+
- Stored locally in your browser (not on server)
|
|
138
|
+
- Click any history item to load it into the editor
|
|
139
|
+
- Stores up to 20 recent queries
|
|
140
|
+
- Clear history with the "Clear" button
|
|
141
|
+
|
|
142
|
+
### Allowed Queries
|
|
143
|
+
|
|
144
|
+
✅ **Allowed**:
|
|
145
|
+
- `SELECT * FROM users`
|
|
146
|
+
- `SELECT id, name FROM users WHERE active = true`
|
|
147
|
+
- `WITH active_users AS (SELECT * FROM users WHERE active = true) SELECT * FROM active_users`
|
|
148
|
+
- Queries with JOINs, ORDER BY, GROUP BY, etc.
|
|
149
|
+
|
|
150
|
+
❌ **Blocked**:
|
|
151
|
+
- `UPDATE users SET name = 'test'`
|
|
152
|
+
- `DELETE FROM users`
|
|
153
|
+
- `INSERT INTO users VALUES (...)`
|
|
154
|
+
- `DROP TABLE users`
|
|
155
|
+
- `SELECT * FROM users; DELETE FROM users` (multiple statements)
|
|
156
|
+
- Any query containing forbidden keywords
|
|
157
|
+
|
|
158
|
+
## Example Queries
|
|
159
|
+
|
|
160
|
+
```sql
|
|
161
|
+
-- List recent users
|
|
162
|
+
SELECT id, email, created_at
|
|
163
|
+
FROM users
|
|
164
|
+
ORDER BY created_at DESC
|
|
165
|
+
LIMIT 10;
|
|
166
|
+
|
|
167
|
+
-- Count by status
|
|
168
|
+
SELECT status, COUNT(*) as count
|
|
169
|
+
FROM orders
|
|
170
|
+
GROUP BY status;
|
|
171
|
+
|
|
172
|
+
-- Join with aggregation
|
|
173
|
+
SELECT u.email, COUNT(o.id) as order_count
|
|
174
|
+
FROM users u
|
|
175
|
+
LEFT JOIN orders o ON u.id = o.user_id
|
|
176
|
+
GROUP BY u.id, u.email
|
|
177
|
+
ORDER BY order_count DESC
|
|
178
|
+
LIMIT 20;
|
|
179
|
+
|
|
180
|
+
-- Common Table Expression (CTE)
|
|
181
|
+
WITH active_users AS (
|
|
182
|
+
SELECT * FROM users WHERE active = true
|
|
183
|
+
)
|
|
184
|
+
SELECT * FROM active_users WHERE created_at > DATE('now', '-30 days');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Security Considerations
|
|
188
|
+
|
|
189
|
+
### Environment Configuration
|
|
190
|
+
|
|
191
|
+
⚠️ **Important**: QueryConsole is **disabled by default in production**. To enable in non-development environments:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
config.enabled_environments = %w[development staging]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**Never enable in production without**:
|
|
198
|
+
1. Strong authentication
|
|
199
|
+
2. Network restrictions (VPN, IP whitelist)
|
|
200
|
+
3. Audit monitoring
|
|
201
|
+
4. Database read-only user (recommended)
|
|
202
|
+
|
|
203
|
+
### Authorization
|
|
204
|
+
|
|
205
|
+
The authorization hook is called on **every request**. Ensure it:
|
|
206
|
+
- Returns `false` or `nil` to deny access
|
|
207
|
+
- Is performant (avoid N+1 queries)
|
|
208
|
+
- Handles edge cases (logged out users, etc.)
|
|
209
|
+
|
|
210
|
+
### Audit Logs
|
|
211
|
+
|
|
212
|
+
All queries are logged to `Rails.logger.info` with:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
{
|
|
216
|
+
component: "query_console",
|
|
217
|
+
actor: "user@example.com",
|
|
218
|
+
sql: "SELECT * FROM users LIMIT 10",
|
|
219
|
+
duration_ms: 45.2,
|
|
220
|
+
rows: 10,
|
|
221
|
+
status: "ok", # or "error"
|
|
222
|
+
truncated: false
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Monitor these logs for:
|
|
227
|
+
- Unusual query patterns
|
|
228
|
+
- Unauthorized access attempts
|
|
229
|
+
- Performance issues
|
|
230
|
+
- Data access patterns
|
|
231
|
+
|
|
232
|
+
### Database Permissions
|
|
233
|
+
|
|
234
|
+
For production environments, consider using a dedicated read-only database user:
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# config/database.yml
|
|
238
|
+
production_readonly:
|
|
239
|
+
<<: *production
|
|
240
|
+
username: readonly_user
|
|
241
|
+
password: <%= ENV['READONLY_DB_PASSWORD'] %>
|
|
242
|
+
|
|
243
|
+
# In your initializer
|
|
244
|
+
QueryConsole.configure do |config|
|
|
245
|
+
config.database_config = :production_readonly
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Development
|
|
250
|
+
|
|
251
|
+
### Testing Without a Rails App
|
|
252
|
+
|
|
253
|
+
You can test the gem in isolation without needing a full Rails application:
|
|
254
|
+
|
|
255
|
+
**Option 1: Run automated tests**
|
|
256
|
+
```bash
|
|
257
|
+
cd query_console
|
|
258
|
+
bundle install
|
|
259
|
+
bundle exec rspec
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Option 2: Start the test server**
|
|
263
|
+
```bash
|
|
264
|
+
cd query_console
|
|
265
|
+
bundle install
|
|
266
|
+
./bin/test_server
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Then visit: http://localhost:9292/query_console
|
|
270
|
+
|
|
271
|
+
The test server includes sample data and is pre-configured for easy testing.
|
|
272
|
+
|
|
273
|
+
**See [TESTING.md](TESTING.md) for detailed testing instructions.**
|
|
274
|
+
|
|
275
|
+
### Test Coverage
|
|
276
|
+
|
|
277
|
+
The test suite includes:
|
|
278
|
+
- SQL validator specs (security rules)
|
|
279
|
+
- SQL limiter specs (result limiting)
|
|
280
|
+
- Runner specs (integration tests)
|
|
281
|
+
- Controller specs (authorization & routing)
|
|
282
|
+
|
|
283
|
+
## Frontend Technology Stack
|
|
284
|
+
|
|
285
|
+
QueryConsole uses **Hotwire (Turbo + Stimulus)**, the modern Rails-native frontend framework:
|
|
286
|
+
|
|
287
|
+
### What's Included
|
|
288
|
+
|
|
289
|
+
- **Turbo Frames**: Query results update without page reloads (SPA-like experience)
|
|
290
|
+
- **Stimulus Controllers**: Organized JavaScript for collapsible sections, history, and editor
|
|
291
|
+
- **CDN Delivery**: Hotwire loaded from CDN (no asset compilation needed)
|
|
292
|
+
- **Zero Build Step**: No webpack, esbuild, or other bundlers required
|
|
293
|
+
|
|
294
|
+
### Architecture
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
Frontend Stack
|
|
298
|
+
├── HTML: ERB Templates
|
|
299
|
+
├── CSS: Vanilla CSS (inline)
|
|
300
|
+
├── JavaScript:
|
|
301
|
+
│ ├── Turbo Frames (results updates)
|
|
302
|
+
│ └── Stimulus Controllers
|
|
303
|
+
│ ├── collapsible_controller (section toggling)
|
|
304
|
+
│ ├── history_controller (localStorage management)
|
|
305
|
+
│ └── editor_controller (query execution)
|
|
306
|
+
└── Storage: localStorage API
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Benefits
|
|
310
|
+
|
|
311
|
+
✅ **No Build Step**: Works out of the box, no compilation needed
|
|
312
|
+
✅ **Rails-Native**: Standard Rails 7+ approach
|
|
313
|
+
✅ **Lightweight**: ~50KB total (vs React's 200KB+)
|
|
314
|
+
✅ **Fast**: No page reloads, instant interactions
|
|
315
|
+
✅ **Progressive**: Degrades gracefully without JavaScript
|
|
316
|
+
|
|
317
|
+
### Why Hotwire?
|
|
318
|
+
|
|
319
|
+
1. **Rails Standard**: Default frontend stack for Rails 7+
|
|
320
|
+
2. **Simple**: Fewer moving parts than SPA frameworks
|
|
321
|
+
3. **Productive**: Write less JavaScript, more HTML
|
|
322
|
+
4. **Modern**: All the benefits of SPAs without the complexity
|
|
323
|
+
5. **Maintainable**: Standard Rails patterns throughout
|
|
324
|
+
|
|
325
|
+
## Troubleshooting
|
|
326
|
+
|
|
327
|
+
### Console returns 404
|
|
328
|
+
|
|
329
|
+
**Possible causes**:
|
|
330
|
+
1. Environment not in `enabled_environments`
|
|
331
|
+
2. Authorization hook returns `false`
|
|
332
|
+
3. Authorization hook not configured (defaults to deny)
|
|
333
|
+
|
|
334
|
+
**Solution**: Check your initializer configuration.
|
|
335
|
+
|
|
336
|
+
### Query times out
|
|
337
|
+
|
|
338
|
+
**Causes**:
|
|
339
|
+
- Query is too complex
|
|
340
|
+
- Database is slow
|
|
341
|
+
- Timeout setting too aggressive
|
|
342
|
+
|
|
343
|
+
**Solutions**:
|
|
344
|
+
- Increase `timeout_ms`
|
|
345
|
+
- Optimize query
|
|
346
|
+
- Add indexes to database
|
|
347
|
+
|
|
348
|
+
### "Multiple statements" error
|
|
349
|
+
|
|
350
|
+
**Cause**: Query contains semicolon (`;`) in the middle
|
|
351
|
+
|
|
352
|
+
**Solution**: Remove extra semicolons. Only one trailing semicolon is allowed.
|
|
353
|
+
|
|
354
|
+
## Contributing
|
|
355
|
+
|
|
356
|
+
1. Fork the repository
|
|
357
|
+
2. Create your feature branch (`git checkout -b feature/my-feature`)
|
|
358
|
+
3. Write tests for your changes
|
|
359
|
+
4. Ensure tests pass (`bundle exec rspec`)
|
|
360
|
+
5. Commit your changes (`git commit -am 'Add feature'`)
|
|
361
|
+
6. Push to the branch (`git push origin feature/my-feature`)
|
|
362
|
+
7. Create a Pull Request
|
|
363
|
+
|
|
364
|
+
## License
|
|
365
|
+
|
|
366
|
+
The gem is available as open source under the terms of the [MIT License](MIT-LICENSE).
|
|
367
|
+
|
|
368
|
+
## Credits
|
|
369
|
+
|
|
370
|
+
Created by [Johnson Gnanasekar](https://github.com/JohnsonGnanasekar)
|
|
371
|
+
|
|
372
|
+
## Changelog
|
|
373
|
+
|
|
374
|
+
### 0.1.0 (Initial Release)
|
|
375
|
+
|
|
376
|
+
- Basic query console with read-only enforcement
|
|
377
|
+
- Environment gating and authorization hooks
|
|
378
|
+
- SQL validation and row limiting
|
|
379
|
+
- Query timeout protection
|
|
380
|
+
- Client-side history with localStorage
|
|
381
|
+
- Comprehensive test suite
|
|
382
|
+
- Audit logging
|
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module QueryConsole
|
|
2
|
+
class ApplicationController < ActionController::Base
|
|
3
|
+
# Rails 8 CSRF protection - use null_session for Turbo Frame requests
|
|
4
|
+
protect_from_forgery with: :exception, prepend: true
|
|
5
|
+
|
|
6
|
+
# Skip CSRF for Turbo Frame requests (Turbo handles it via meta tags)
|
|
7
|
+
skip_forgery_protection if: -> { request.headers['Turbo-Frame'].present? }
|
|
8
|
+
|
|
9
|
+
before_action :ensure_enabled!
|
|
10
|
+
before_action :authorize_access!
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def ensure_enabled!
|
|
15
|
+
config = QueryConsole.configuration
|
|
16
|
+
|
|
17
|
+
unless config.enabled_environments.map(&:to_s).include?(Rails.env.to_s)
|
|
18
|
+
raise ActionController::RoutingError, "Not Found"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def authorize_access!
|
|
23
|
+
config = QueryConsole.configuration
|
|
24
|
+
|
|
25
|
+
# Default deny if no authorize hook is configured
|
|
26
|
+
if config.authorize.nil?
|
|
27
|
+
Rails.logger.warn("[QueryConsole] Access denied: No authorization hook configured")
|
|
28
|
+
raise ActionController::RoutingError, "Not Found"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Call the authorization hook
|
|
32
|
+
unless config.authorize.call(self)
|
|
33
|
+
Rails.logger.warn("[QueryConsole] Access denied by authorization hook")
|
|
34
|
+
raise ActionController::RoutingError, "Not Found"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module QueryConsole
|
|
2
|
+
class QueriesController < ApplicationController
|
|
3
|
+
def new
|
|
4
|
+
# Render the main query editor page
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
sql = params[:sql]
|
|
9
|
+
|
|
10
|
+
if sql.blank?
|
|
11
|
+
@result = Runner::QueryResult.new(error: "Query cannot be empty")
|
|
12
|
+
respond_to do |format|
|
|
13
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("query-results", partial: "results", locals: { result: @result }) }
|
|
14
|
+
format.html { render :_results, layout: false }
|
|
15
|
+
end
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Execute the query
|
|
20
|
+
runner = Runner.new(sql)
|
|
21
|
+
@result = runner.execute
|
|
22
|
+
|
|
23
|
+
# Log the query execution
|
|
24
|
+
AuditLogger.log_query(
|
|
25
|
+
sql: sql,
|
|
26
|
+
result: @result,
|
|
27
|
+
controller: self
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Respond with Turbo Stream or HTML
|
|
31
|
+
respond_to do |format|
|
|
32
|
+
format.turbo_stream do
|
|
33
|
+
render turbo_stream: turbo_stream.replace(
|
|
34
|
+
"query-results",
|
|
35
|
+
partial: "results",
|
|
36
|
+
locals: { result: @result }
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
format.html { render :_results, layout: false }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Entry point for the QueryConsole Stimulus application
|
|
2
|
+
import { Application } from "@hotwired/stimulus"
|
|
3
|
+
import { registerControllers } from "@hotwired/stimulus-loading"
|
|
4
|
+
|
|
5
|
+
const application = Application.start()
|
|
6
|
+
|
|
7
|
+
// Configure Stimulus development experience
|
|
8
|
+
application.debug = false
|
|
9
|
+
window.Stimulus = application
|
|
10
|
+
|
|
11
|
+
// Register all controllers in the controllers/query_console directory
|
|
12
|
+
import CollapsibleController from "./controllers/collapsible_controller"
|
|
13
|
+
import HistoryController from "./controllers/history_controller"
|
|
14
|
+
import EditorController from "./controllers/editor_controller"
|
|
15
|
+
|
|
16
|
+
application.register("collapsible", CollapsibleController)
|
|
17
|
+
application.register("history", HistoryController)
|
|
18
|
+
application.register("editor", EditorController)
|
|
19
|
+
|
|
20
|
+
export { application }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Handles collapsible sections (banner, editor, history)
|
|
4
|
+
// Usage: <div data-controller="collapsible" data-collapsible-key-value="banner">
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static values = {
|
|
7
|
+
key: String // Storage key suffix (e.g., "banner", "editor", "history")
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.storageKey = `query_console.${this.keyValue}_collapsed`
|
|
12
|
+
this.loadState()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
toggle(event) {
|
|
16
|
+
event.preventDefault()
|
|
17
|
+
this.element.classList.toggle('collapsed')
|
|
18
|
+
|
|
19
|
+
const isCollapsed = this.element.classList.contains('collapsed')
|
|
20
|
+
this.saveState(isCollapsed)
|
|
21
|
+
this.updateToggleButton(event.target, isCollapsed)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
loadState() {
|
|
25
|
+
const isCollapsed = localStorage.getItem(this.storageKey) === 'true'
|
|
26
|
+
if (isCollapsed) {
|
|
27
|
+
this.element.classList.add('collapsed')
|
|
28
|
+
const button = this.element.querySelector('.section-toggle, .banner-toggle')
|
|
29
|
+
if (button) {
|
|
30
|
+
this.updateToggleButton(button, true)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
saveState(isCollapsed) {
|
|
36
|
+
localStorage.setItem(this.storageKey, isCollapsed ? 'true' : 'false')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
updateToggleButton(button, isCollapsed) {
|
|
40
|
+
button.textContent = isCollapsed ? '▲' : '▼'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Manages the SQL editor textarea and query execution
|
|
4
|
+
// Usage: <div data-controller="editor">
|
|
5
|
+
export default class extends Controller {
|
|
6
|
+
static targets = ["textarea", "runButton", "clearButton", "results"]
|
|
7
|
+
|
|
8
|
+
connect() {
|
|
9
|
+
// Listen for history load events
|
|
10
|
+
this.element.addEventListener('history:load', (event) => {
|
|
11
|
+
this.loadQuery(event.detail.sql)
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Load query into textarea (from history)
|
|
16
|
+
loadQuery(sql) {
|
|
17
|
+
this.textareaTarget.value = sql
|
|
18
|
+
this.textareaTarget.focus()
|
|
19
|
+
|
|
20
|
+
// Scroll to editor
|
|
21
|
+
this.element.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Clear textarea
|
|
25
|
+
clear(event) {
|
|
26
|
+
event.preventDefault()
|
|
27
|
+
this.textareaTarget.value = ''
|
|
28
|
+
this.textareaTarget.focus()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle form submission
|
|
32
|
+
submit(event) {
|
|
33
|
+
const sql = this.textareaTarget.value.trim()
|
|
34
|
+
|
|
35
|
+
if (!sql) {
|
|
36
|
+
event.preventDefault()
|
|
37
|
+
alert('Please enter a SQL query')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Show loading state
|
|
42
|
+
this.runButtonTarget.disabled = true
|
|
43
|
+
this.runButtonTarget.textContent = 'Running...'
|
|
44
|
+
|
|
45
|
+
// After Turbo completes the request, we'll handle success/error
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Called after successful query execution (via Turbo)
|
|
49
|
+
querySuccess(event) {
|
|
50
|
+
// Re-enable button
|
|
51
|
+
this.runButtonTarget.disabled = false
|
|
52
|
+
this.runButtonTarget.textContent = 'Run Query'
|
|
53
|
+
|
|
54
|
+
// Dispatch event to add to history
|
|
55
|
+
const sql = this.textareaTarget.value.trim()
|
|
56
|
+
this.dispatch('executed', {
|
|
57
|
+
detail: {
|
|
58
|
+
sql: sql,
|
|
59
|
+
timestamp: new Date().toISOString()
|
|
60
|
+
},
|
|
61
|
+
target: document.querySelector('[data-controller="history"]')
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Called after failed query execution
|
|
66
|
+
queryError(event) {
|
|
67
|
+
this.runButtonTarget.disabled = false
|
|
68
|
+
this.runButtonTarget.textContent = 'Run Query'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Handle Turbo Frame errors
|
|
72
|
+
turboFrameError(event) {
|
|
73
|
+
console.error('Turbo Frame error:', event.detail)
|
|
74
|
+
this.runButtonTarget.disabled = false
|
|
75
|
+
this.runButtonTarget.textContent = 'Run Query'
|
|
76
|
+
}
|
|
77
|
+
}
|