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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c2f6153e25093f6b47ff480dcad0305d3729725541ce61fe814c5551f52ad3f
4
- data.tar.gz: e81abfd30bf2a212a5f8ca47847720a3760358c58cf5da14c659519e4d068c35
3
+ metadata.gz: 3c4f017351c420cd97ca5aecea866a3dee50f818d147a4b60bf6965461763ab7
4
+ data.tar.gz: 487cdcf1f040fe3d76f323037f4923efacb62064d0026ec8290cc6a2a8fc5065
5
5
  SHA512:
6
- metadata.gz: 4867b41518f09623a58584e9130aab9314d89366e9d97d0a5b8826d953156d20004ba3df88087e459bd0757e263331bb71bb14a149d6eb49caf6e8159ef63d15
7
- data.tar.gz: 30676022f6642b95b55658364d072a1bb282e0df36c5c01e4f158cefac29af0465d7697b813b5b40814cba45c444bd99edfee818e1c3555a456d46b5eaa91b01
6
+ metadata.gz: d18c796d4242d5710c53a1c7325c2938128a7fb91875742ab7e7cbfe706ccb3ee0b703e83067f6a6ab434812d628f3c336822720b4cdbdc6ac3c9f552f5600e2
7
+ data.tar.gz: 290bec88c2684b6d348350348c121e270bf85edda4e36c15d958b5f13e7564305ebaee4a8cf650391af4e486c0b940c3fad0f7987afb973bc9632ba564825dc4
data/.env.example ADDED
@@ -0,0 +1,5 @@
1
+ # PostgreSQL connection URL for Umami database
2
+ DATABASE_URL=postgresql://username:password@localhost:5432/umami_db
3
+
4
+ # Or with more options:
5
+ # DATABASE_URL=postgresql://username:password@host:5432/database?sslmode=require&connect_timeout=10
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
- Then configure the gem in an initializer (e.g., `config/initializers/umami_read_models.rb`):
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
- For read replicas:
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
- This uses Rails' built-in database roles. Even though the models are read-only, Rails still requires
73
- a `:writing` connection to be defined. Both connections can point to the same database, or you can
74
- use a read replica for the `:reading` connection for better performance.
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 by default. Any attempt to create, update, or delete records will fail:
190
+ All models are read-only. Any attempt to create, update, or delete records will raise an error:
201
191
 
202
192
  ```ruby
203
- # This will raise an error
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
- # This will also raise an error
208
- Umami::Models::Website.find(id).update(name: "New Name")
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
- results = Umami::Models::Base.connection.execute(<<-SQL)
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 = '#{website_id}'
241
- AND created_at >= '#{30.days.ago}'
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Umami
4
4
  module Models
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
6
6
  end
7
7
  end
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 :table_prefix, :database
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
- def self.table_name
39
- "#{Umami::Models.table_prefix}#{super}"
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.0
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: []