umami-read-models 0.1.0 → 0.1.1
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/.env.example +5 -0
- data/CHANGELOG.md +18 -0
- data/README.md +25 -25
- data/lib/umami/models/version.rb +1 -1
- data/lib/umami/models.rb +21 -12
- data/scripts/README.md +35 -0
- data/scripts/validate_connection.rb +228 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c4f017351c420cd97ca5aecea866a3dee50f818d147a4b60bf6965461763ab7
|
4
|
+
data.tar.gz: 487cdcf1f040fe3d76f323037f4923efacb62064d0026ec8290cc6a2a8fc5065
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d18c796d4242d5710c53a1c7325c2938128a7fb91875742ab7e7cbfe706ccb3ee0b703e83067f6a6ab434812d628f3c336822720b4cdbdc6ac3c9f552f5600e2
|
7
|
+
data.tar.gz: 290bec88c2684b6d348350348c121e270bf85edda4e36c15d958b5f13e7564305ebaee4a8cf650391af4e486c0b940c3fad0f7987afb973bc9632ba564825dc4
|
data/.env.example
ADDED
data/CHANGELOG.md
CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
+
## [0.1.1] - 2024-01-26
|
11
|
+
|
12
|
+
### Fixed
|
13
|
+
- Removed non-functional table prefix feature that was documented but didn't work
|
14
|
+
- Fixed database connection configuration to properly work with Rails multi-database
|
15
|
+
- Enhanced read-only protection to prevent all write operations (create, update, delete)
|
16
|
+
- Fixed SQL injection vulnerability in README raw SQL example
|
17
|
+
- Updated documentation to accurately reflect actual gem functionality
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
- Simplified database configuration to use standard Rails patterns
|
21
|
+
- Improved error messages for read-only violations
|
22
|
+
- Database configuration now properly converts simple symbols to Rails multi-db format
|
23
|
+
|
24
|
+
### Removed
|
25
|
+
- Removed broken `table_prefix` configuration option
|
26
|
+
- Removed misleading "thread-safe connection management" claim
|
27
|
+
|
10
28
|
## [0.1.0] - 2024-01-26
|
11
29
|
|
12
30
|
### Added
|
data/README.md
CHANGED
@@ -5,11 +5,9 @@ A Ruby gem that provides read-only ActiveRecord models for accessing Umami Analy
|
|
5
5
|
## Features
|
6
6
|
|
7
7
|
- Read-only ActiveRecord models for all Umami database tables
|
8
|
-
- Support for PostgreSQL connections
|
8
|
+
- Support for PostgreSQL connections
|
9
9
|
- Built-in query scopes for common analytics queries
|
10
10
|
- Association mappings between models
|
11
|
-
- Configurable table prefix support
|
12
|
-
- Thread-safe connection management
|
13
11
|
|
14
12
|
## Installation
|
15
13
|
|
@@ -47,31 +45,23 @@ production:
|
|
47
45
|
password: <%= ENV['UMAMI_DB_PASSWORD'] %>
|
48
46
|
```
|
49
47
|
|
50
|
-
|
48
|
+
Configure the gem in an initializer (e.g., `config/initializers/umami_read_models.rb`):
|
51
49
|
|
52
50
|
```ruby
|
51
|
+
# For a simple setup with one database
|
53
52
|
Umami::Models.configure do |config|
|
54
|
-
# Specify which database configuration to use
|
55
53
|
config.database = :umami
|
56
|
-
|
57
|
-
# Optional: Set a table prefix if your Umami tables use one
|
58
|
-
config.table_prefix = "umami_"
|
59
54
|
end
|
60
|
-
```
|
61
|
-
|
62
|
-
### Advanced Multi-Database Configuration
|
63
55
|
|
64
|
-
|
65
|
-
|
66
|
-
```ruby
|
56
|
+
# Or for read replicas
|
67
57
|
Umami::Models.configure do |config|
|
68
58
|
config.database = { writing: :umami, reading: :umami_replica }
|
69
59
|
end
|
70
60
|
```
|
71
61
|
|
72
|
-
|
73
|
-
|
74
|
-
|
62
|
+
**Important**: The configuration must be set during application initialization, not in an `after_initialize` block.
|
63
|
+
|
64
|
+
**Note about read replicas**: Even though the models are read-only, Rails still requires a `:writing` connection to be defined. Both connections can point to the same database if you don't have a read replica.
|
75
65
|
|
76
66
|
## Usage
|
77
67
|
|
@@ -197,15 +187,19 @@ params = report.parsed_parameters
|
|
197
187
|
|
198
188
|
## Read-Only Protection
|
199
189
|
|
200
|
-
All models are read-only
|
190
|
+
All models are read-only. Any attempt to create, update, or delete records will raise an error:
|
201
191
|
|
202
192
|
```ruby
|
203
|
-
#
|
193
|
+
# Creating records will raise an error
|
204
194
|
website = Umami::Models::Website.new(name: "Test")
|
205
195
|
website.save # => raises ActiveRecord::ReadOnlyRecord
|
206
196
|
|
207
|
-
#
|
208
|
-
Umami::Models::Website.find(id)
|
197
|
+
# Updating records will raise an error
|
198
|
+
website = Umami::Models::Website.find(id)
|
199
|
+
website.update(name: "New Name") # => raises ActiveRecord::ReadOnlyRecord
|
200
|
+
|
201
|
+
# Deleting records will raise an error
|
202
|
+
website.destroy # => raises ActiveRecord::ReadOnlyRecord
|
209
203
|
```
|
210
204
|
|
211
205
|
## Advanced Usage
|
@@ -228,20 +222,26 @@ Umami::Models::WebsiteEvent
|
|
228
222
|
|
229
223
|
### Raw SQL
|
230
224
|
|
231
|
-
For complex analytics queries, you can use raw SQL:
|
225
|
+
For complex analytics queries, you can use raw SQL with proper parameterization:
|
232
226
|
|
233
227
|
```ruby
|
234
|
-
|
228
|
+
sql = <<-SQL
|
235
229
|
SELECT
|
236
230
|
DATE(created_at) as date,
|
237
231
|
COUNT(DISTINCT session_id) as visitors,
|
238
232
|
COUNT(*) as page_views
|
239
233
|
FROM website_event
|
240
|
-
WHERE website_id =
|
241
|
-
AND created_at >=
|
234
|
+
WHERE website_id = ?
|
235
|
+
AND created_at >= ?
|
242
236
|
GROUP BY DATE(created_at)
|
243
237
|
ORDER BY date DESC
|
244
238
|
SQL
|
239
|
+
|
240
|
+
results = Umami::Models::Base.connection.exec_query(
|
241
|
+
sql,
|
242
|
+
'SQL',
|
243
|
+
[[nil, website_id], [nil, 30.days.ago]]
|
244
|
+
)
|
245
245
|
```
|
246
246
|
|
247
247
|
## Development
|
data/lib/umami/models/version.rb
CHANGED
data/lib/umami/models.rb
CHANGED
@@ -9,34 +9,43 @@ module Umami
|
|
9
9
|
class Error < StandardError; end
|
10
10
|
|
11
11
|
class << self
|
12
|
-
attr_accessor :
|
12
|
+
attr_accessor :database
|
13
13
|
|
14
14
|
def configure
|
15
15
|
yield self
|
16
|
+
apply_configuration! if database
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply_configuration!
|
20
|
+
return unless database
|
21
|
+
|
22
|
+
# Apply to Base and all its descendants
|
23
|
+
if database.is_a?(Hash)
|
24
|
+
Base.connects_to database: database
|
25
|
+
else
|
26
|
+
Base.connects_to database: { writing: database, reading: database }
|
27
|
+
end
|
16
28
|
end
|
17
29
|
end
|
18
30
|
|
19
|
-
self.table_prefix = ""
|
20
31
|
self.database = nil
|
21
32
|
|
22
33
|
# Base class for all Umami models with read-only enforcement
|
23
34
|
class Base < ActiveRecord::Base
|
24
35
|
self.abstract_class = true
|
25
36
|
|
26
|
-
def self.inherited(subclass)
|
27
|
-
super
|
28
|
-
# Apply the database configuration when a model inherits from Base
|
29
|
-
return unless Umami::Models.database
|
30
|
-
|
31
|
-
subclass.connects_to database: Umami::Models.database
|
32
|
-
end
|
33
|
-
|
34
37
|
def readonly?
|
35
38
|
true
|
36
39
|
end
|
37
40
|
|
38
|
-
|
39
|
-
|
41
|
+
# Prevent any write operations
|
42
|
+
before_save :prevent_writes
|
43
|
+
before_destroy :prevent_writes
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def prevent_writes
|
48
|
+
raise ActiveRecord::ReadOnlyRecord, "Umami models are read-only"
|
40
49
|
end
|
41
50
|
end
|
42
51
|
end
|
data/scripts/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Scripts
|
2
|
+
|
3
|
+
## validate_connection.rb
|
4
|
+
|
5
|
+
A validation script to test the umami-read-models gem functionality against a real Umami database.
|
6
|
+
|
7
|
+
### Setup
|
8
|
+
|
9
|
+
1. Create a `.env` file in the gem root directory:
|
10
|
+
```bash
|
11
|
+
DATABASE_URL=postgresql://user:password@host:port/umami_database
|
12
|
+
```
|
13
|
+
|
14
|
+
2. Install dependencies:
|
15
|
+
```bash
|
16
|
+
bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
### Usage
|
20
|
+
|
21
|
+
```bash
|
22
|
+
ruby scripts/validate_connection.rb
|
23
|
+
```
|
24
|
+
|
25
|
+
### What it tests
|
26
|
+
|
27
|
+
- Database connectivity
|
28
|
+
- Model loading
|
29
|
+
- Basic queries (record counts)
|
30
|
+
- Associations between models
|
31
|
+
- Query scopes
|
32
|
+
- Read-only protection
|
33
|
+
- Complex analytical queries
|
34
|
+
|
35
|
+
The script provides colored output showing pass/fail status for each test.
|
@@ -0,0 +1,228 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Script to validate umami-read-models gem functionality
|
5
|
+
# Usage: ruby scripts/validate_connection.rb
|
6
|
+
# Requires: DATABASE_URL environment variable
|
7
|
+
|
8
|
+
require "bundler/setup"
|
9
|
+
require "umami/models"
|
10
|
+
require "active_record"
|
11
|
+
require "dotenv/load"
|
12
|
+
require "uri"
|
13
|
+
|
14
|
+
# Colors for output
|
15
|
+
class String
|
16
|
+
def green = "\e[32m#{self}\e[0m"
|
17
|
+
def red = "\e[31m#{self}\e[0m"
|
18
|
+
def yellow = "\e[33m#{self}\e[0m"
|
19
|
+
def blue = "\e[34m#{self}\e[0m"
|
20
|
+
end
|
21
|
+
|
22
|
+
puts "Umami Read Models - Connection Validation".blue
|
23
|
+
puts "=" * 50
|
24
|
+
|
25
|
+
# Check for DATABASE_URL
|
26
|
+
unless ENV["DATABASE_URL"]
|
27
|
+
puts "ERROR: DATABASE_URL not found in environment".red
|
28
|
+
puts "Please create a .env file with:"
|
29
|
+
puts "DATABASE_URL=postgresql://user:password@host:port/database"
|
30
|
+
exit 1
|
31
|
+
end
|
32
|
+
|
33
|
+
puts "✓ DATABASE_URL found".green
|
34
|
+
|
35
|
+
# Since we're using DATABASE_URL directly, we don't need Rails database config
|
36
|
+
# Just use the default connection
|
37
|
+
ActiveRecord::Base.establish_connection(ENV.fetch("DATABASE_URL", nil))
|
38
|
+
|
39
|
+
# Test configuration - skip this since we're not in a Rails app with named databases
|
40
|
+
puts "\n1. Testing database connection...".yellow
|
41
|
+
begin
|
42
|
+
ActiveRecord::Base.connection.execute("SELECT 1")
|
43
|
+
puts "✓ Database connection working".green
|
44
|
+
rescue StandardError => e
|
45
|
+
puts "✗ Database connection failed: #{e.message}".red
|
46
|
+
exit 1
|
47
|
+
end
|
48
|
+
|
49
|
+
# Test Rails-style configuration (for use in Rails apps)
|
50
|
+
puts "\n2. Testing Rails-style database configuration...".yellow
|
51
|
+
begin
|
52
|
+
# Parse the DATABASE_URL to get connection parameters
|
53
|
+
uri = URI.parse(ENV.fetch("DATABASE_URL", nil))
|
54
|
+
db_config_hash = {
|
55
|
+
adapter: uri.scheme == "postgres" ? "postgresql" : uri.scheme,
|
56
|
+
host: uri.host,
|
57
|
+
port: uri.port || 5432,
|
58
|
+
database: uri.path[1..], # Remove leading slash
|
59
|
+
username: uri.user,
|
60
|
+
password: uri.password
|
61
|
+
}
|
62
|
+
|
63
|
+
# Add any query parameters (like sslmode, channel_binding, etc.)
|
64
|
+
if uri.query
|
65
|
+
URI.decode_www_form(uri.query).each do |key, value|
|
66
|
+
db_config_hash[key.to_sym] = value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Create the configuration using the current environment
|
71
|
+
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
|
72
|
+
db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
|
73
|
+
current_env,
|
74
|
+
"umami",
|
75
|
+
db_config_hash
|
76
|
+
)
|
77
|
+
|
78
|
+
# Register this configuration
|
79
|
+
ActiveRecord::Base.configurations = {
|
80
|
+
current_env => { "umami" => db_config.configuration_hash }
|
81
|
+
}
|
82
|
+
|
83
|
+
# Now test the gem's configuration
|
84
|
+
Umami::Models.configure do |config|
|
85
|
+
config.database = :umami
|
86
|
+
end
|
87
|
+
|
88
|
+
# Verify it works by testing a query
|
89
|
+
Umami::Models::Website.connection.execute("SELECT 1")
|
90
|
+
puts "✓ Rails-style configuration working".green
|
91
|
+
puts " (In a Rails app, use: config.database = :umami)".green
|
92
|
+
|
93
|
+
# Also test multi-database configuration
|
94
|
+
Umami::Models.configure do |config|
|
95
|
+
config.database = { writing: :umami, reading: :umami }
|
96
|
+
end
|
97
|
+
Umami::Models::Website.connection.execute("SELECT 1")
|
98
|
+
puts "✓ Multi-database configuration also works".green
|
99
|
+
puts " (For read replicas: { writing: :umami, reading: :umami_replica })".green
|
100
|
+
rescue StandardError => e
|
101
|
+
puts "✗ Rails-style configuration failed: #{e.message}".red
|
102
|
+
puts " Note: This is optional - direct DATABASE_URL connection still works".yellow
|
103
|
+
end
|
104
|
+
|
105
|
+
# Test model loading
|
106
|
+
puts "\n3. Testing model loading...".yellow
|
107
|
+
models = [
|
108
|
+
Umami::Models::User,
|
109
|
+
Umami::Models::Website,
|
110
|
+
Umami::Models::Session,
|
111
|
+
Umami::Models::WebsiteEvent,
|
112
|
+
Umami::Models::EventData,
|
113
|
+
Umami::Models::SessionData,
|
114
|
+
Umami::Models::Team,
|
115
|
+
Umami::Models::TeamUser,
|
116
|
+
Umami::Models::Report
|
117
|
+
]
|
118
|
+
|
119
|
+
models.each do |model|
|
120
|
+
puts " ✓ #{model.name} loaded".green
|
121
|
+
end
|
122
|
+
|
123
|
+
# Test basic queries
|
124
|
+
puts "\n4. Testing basic queries...".yellow
|
125
|
+
begin
|
126
|
+
# Count records
|
127
|
+
puts " Users: #{Umami::Models::User.count}"
|
128
|
+
puts " Websites: #{Umami::Models::Website.count}"
|
129
|
+
puts " Sessions: #{Umami::Models::Session.count}"
|
130
|
+
puts " Events: #{Umami::Models::WebsiteEvent.count}"
|
131
|
+
puts "✓ Basic queries working".green
|
132
|
+
rescue StandardError => e
|
133
|
+
puts "✗ Query failed: #{e.message}".red
|
134
|
+
puts " Make sure the Umami database schema is set up correctly".yellow
|
135
|
+
end
|
136
|
+
|
137
|
+
# Test associations
|
138
|
+
puts "\n4. Testing associations...".yellow
|
139
|
+
begin
|
140
|
+
if (website = Umami::Models::Website.first)
|
141
|
+
puts " Testing website: #{website.name || "Unnamed"}"
|
142
|
+
puts " - Sessions count: #{website.sessions.count}"
|
143
|
+
puts " - Events count: #{website.website_events.count}"
|
144
|
+
puts " - Has user: #{website.user ? "Yes" : "No"}"
|
145
|
+
puts "✓ Associations working".green
|
146
|
+
else
|
147
|
+
puts " No websites found to test associations".yellow
|
148
|
+
end
|
149
|
+
rescue StandardError => e
|
150
|
+
puts "✗ Association test failed: #{e.message}".red
|
151
|
+
end
|
152
|
+
|
153
|
+
# Test scopes
|
154
|
+
puts "\n5. Testing scopes...".yellow
|
155
|
+
begin
|
156
|
+
# Test various scopes
|
157
|
+
puts " Active websites: #{Umami::Models::Website.active.count}"
|
158
|
+
puts " Recent sessions: #{Umami::Models::Session.recent.limit(5).count}"
|
159
|
+
puts " Page views: #{Umami::Models::WebsiteEvent.page_views.count}"
|
160
|
+
puts "✓ Scopes working".green
|
161
|
+
rescue StandardError => e
|
162
|
+
puts "✗ Scope test failed: #{e.message}".red
|
163
|
+
end
|
164
|
+
|
165
|
+
# Test read-only protection
|
166
|
+
puts "\n6. Testing read-only protection...".yellow
|
167
|
+
begin
|
168
|
+
# Test create
|
169
|
+
begin
|
170
|
+
user = Umami::Models::User.new(username: "test")
|
171
|
+
user.save!
|
172
|
+
puts "✗ ERROR: Create should have been blocked!".red
|
173
|
+
rescue ActiveRecord::ReadOnlyRecord => e
|
174
|
+
puts " ✓ Create blocked: #{e.message}".green
|
175
|
+
end
|
176
|
+
|
177
|
+
# Test update
|
178
|
+
if (user = Umami::Models::User.first)
|
179
|
+
begin
|
180
|
+
user.update!(username: "changed")
|
181
|
+
puts "✗ ERROR: Update should have been blocked!".red
|
182
|
+
rescue ActiveRecord::ReadOnlyRecord => e
|
183
|
+
puts " ✓ Update blocked: #{e.message}".green
|
184
|
+
end
|
185
|
+
|
186
|
+
# Test delete
|
187
|
+
begin
|
188
|
+
user.destroy!
|
189
|
+
puts "✗ ERROR: Delete should have been blocked!".red
|
190
|
+
rescue ActiveRecord::ReadOnlyRecord => e
|
191
|
+
puts " ✓ Delete blocked: #{e.message}".green
|
192
|
+
end
|
193
|
+
else
|
194
|
+
puts " No users found to test update/delete protection".yellow
|
195
|
+
end
|
196
|
+
rescue StandardError => e
|
197
|
+
puts "✗ Read-only test failed: #{e.message}".red
|
198
|
+
end
|
199
|
+
|
200
|
+
# Test complex queries
|
201
|
+
puts "\n7. Testing complex queries...".yellow
|
202
|
+
begin
|
203
|
+
# Get top pages for the last 30 days
|
204
|
+
if (website = Umami::Models::Website.first)
|
205
|
+
top_pages = Umami::Models::WebsiteEvent
|
206
|
+
.by_website(website.website_id)
|
207
|
+
.page_views
|
208
|
+
.by_date_range(30.days.ago, Time.current)
|
209
|
+
.group(:url_path)
|
210
|
+
.order("count_all DESC")
|
211
|
+
.limit(5)
|
212
|
+
.count
|
213
|
+
|
214
|
+
puts " Top 5 pages (last 30 days):"
|
215
|
+
top_pages.each do |path, count|
|
216
|
+
puts " - #{path}: #{count} views"
|
217
|
+
end
|
218
|
+
puts "✓ Complex queries working".green if top_pages.any?
|
219
|
+
puts " No page views in the last 30 days".yellow if top_pages.empty?
|
220
|
+
else
|
221
|
+
puts " No websites found for complex query test".yellow
|
222
|
+
end
|
223
|
+
rescue StandardError => e
|
224
|
+
puts "✗ Complex query failed: #{e.message}".red
|
225
|
+
end
|
226
|
+
|
227
|
+
puts "\n#{"=" * 50}"
|
228
|
+
puts "Test completed!".blue
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: umami-read-models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andreas Zeitler
|
@@ -46,6 +46,7 @@ executables: []
|
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
|
+
- ".env.example"
|
49
50
|
- ".rubocop.yml"
|
50
51
|
- CHANGELOG.md
|
51
52
|
- LICENSE.txt
|
@@ -62,6 +63,8 @@ files:
|
|
62
63
|
- lib/umami/models/version.rb
|
63
64
|
- lib/umami/models/website.rb
|
64
65
|
- lib/umami/models/website_event.rb
|
66
|
+
- scripts/README.md
|
67
|
+
- scripts/validate_connection.rb
|
65
68
|
- sig/umami/models.rbs
|
66
69
|
homepage: https://github.com/azeitler/umami-read-models
|
67
70
|
licenses: []
|