dbwatcher 0.1.5 → 1.0.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 +2 -2
- data/app/controllers/dbwatcher/base_controller.rb +95 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +27 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
- data/app/views/dbwatcher/queries/index.html.erb +240 -0
- data/app/views/dbwatcher/sessions/index.html.erb +120 -27
- data/app/views/dbwatcher/sessions/show.html.erb +326 -129
- data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
- data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
- data/app/views/dbwatcher/shared/_header.html.erb +7 -0
- data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
- data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
- data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
- data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
- data/app/views/dbwatcher/tables/changes.html.erb +225 -0
- data/app/views/dbwatcher/tables/index.html.erb +123 -0
- data/app/views/dbwatcher/tables/show.html.erb +86 -0
- data/app/views/layouts/dbwatcher/application.html.erb +375 -26
- data/config/routes.rb +17 -3
- data/lib/dbwatcher/configuration.rb +9 -1
- data/lib/dbwatcher/engine.rb +12 -7
- data/lib/dbwatcher/logging.rb +72 -0
- data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -0
- data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
- data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
- data/lib/dbwatcher/sql_logger.rb +107 -0
- data/lib/dbwatcher/storage/api/base_api.rb +134 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +172 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +134 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +113 -0
- data/lib/dbwatcher/storage/change_processor.rb +65 -0
- data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
- data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
- data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
- data/lib/dbwatcher/storage/date_helper.rb +21 -0
- data/lib/dbwatcher/storage/errors.rb +86 -0
- data/lib/dbwatcher/storage/file_manager.rb +122 -0
- data/lib/dbwatcher/storage/null_session.rb +39 -0
- data/lib/dbwatcher/storage/query_storage.rb +338 -0
- data/lib/dbwatcher/storage/query_validator.rb +24 -0
- data/lib/dbwatcher/storage/session.rb +58 -0
- data/lib/dbwatcher/storage/session_operations.rb +37 -0
- data/lib/dbwatcher/storage/session_query.rb +71 -0
- data/lib/dbwatcher/storage/session_storage.rb +322 -0
- data/lib/dbwatcher/storage/table_storage.rb +237 -0
- data/lib/dbwatcher/storage.rb +112 -85
- data/lib/dbwatcher/tracker.rb +4 -55
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +12 -2
- metadata +47 -1
@@ -0,0 +1,338 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
|
5
|
+
require_relative "query_validator"
|
6
|
+
require_relative "date_helper"
|
7
|
+
|
8
|
+
module Dbwatcher
|
9
|
+
module Storage
|
10
|
+
# Handles persistence and retrieval of database query logs
|
11
|
+
#
|
12
|
+
# This class manages the storage of query data organized by date.
|
13
|
+
# Queries are stored in daily files with automatic cleanup and
|
14
|
+
# size limiting based on configuration. Follows Ruby style guide
|
15
|
+
# patterns for storage class organization.
|
16
|
+
#
|
17
|
+
# @example Basic usage
|
18
|
+
# storage = QueryStorage.new
|
19
|
+
# query = { sql: "SELECT * FROM users", timestamp: Time.current }
|
20
|
+
# storage.save(query)
|
21
|
+
# daily_queries = storage.find_by_date(Date.current)
|
22
|
+
#
|
23
|
+
# @example Advanced filtering
|
24
|
+
# queries = storage.find_by_date_range(1.week.ago..Time.current)
|
25
|
+
# recent_queries = storage.recent(limit: 50)
|
26
|
+
class QueryStorage < BaseStorage
|
27
|
+
# Include shared concerns
|
28
|
+
include Concerns::Validatable
|
29
|
+
include DateHelper
|
30
|
+
|
31
|
+
# Configuration constants
|
32
|
+
DEFAULT_CLEANUP_DAYS = 30
|
33
|
+
QUERIES_DIRECTORY = "queries"
|
34
|
+
MAX_QUERIES_PER_FILE = 1000
|
35
|
+
|
36
|
+
# Validation rules
|
37
|
+
validates_presence_of :sql, :timestamp
|
38
|
+
|
39
|
+
# @return [String] path to queries directory
|
40
|
+
attr_reader :queries_path
|
41
|
+
|
42
|
+
# Initializes query storage with queries directory
|
43
|
+
#
|
44
|
+
# Creates the queries directory if it doesn't exist and sets up
|
45
|
+
# the necessary file structure for date-based organization.
|
46
|
+
#
|
47
|
+
# @param storage_path [String, nil] custom storage path (optional)
|
48
|
+
def initialize(storage_path = nil)
|
49
|
+
super
|
50
|
+
@queries_path = File.join(self.storage_path, QUERIES_DIRECTORY)
|
51
|
+
file_manager.ensure_directory(@queries_path)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Saves a query to date-based storage
|
55
|
+
#
|
56
|
+
# Validates the query and stores it in a daily file. Automatically
|
57
|
+
# applies size limits based on configuration to prevent excessive
|
58
|
+
# storage usage.
|
59
|
+
#
|
60
|
+
# @param query [Hash] query data containing at least :sql and :timestamp
|
61
|
+
# @return [Boolean] true if saved successfully, false if invalid
|
62
|
+
# @raise [ValidationError] if query data is invalid and strict mode enabled
|
63
|
+
#
|
64
|
+
# @example
|
65
|
+
# query = { sql: "SELECT * FROM users", timestamp: Time.current }
|
66
|
+
# storage.save(query) # => true
|
67
|
+
def save(query)
|
68
|
+
query_data = normalize_query_data(query)
|
69
|
+
return false unless QueryValidator.valid?(query_data)
|
70
|
+
|
71
|
+
date = format_date(query_data[:timestamp])
|
72
|
+
query_file = date_file_path(@queries_path, date)
|
73
|
+
|
74
|
+
queries = load_queries_from_file(query_file)
|
75
|
+
queries = add_query_with_limits(queries, query_data)
|
76
|
+
|
77
|
+
safe_write_json(query_file, queries)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Alternative save method that raises on failure
|
81
|
+
#
|
82
|
+
# @param query [Hash] query data to save
|
83
|
+
# @return [Boolean] true if saved successfully
|
84
|
+
# @raise [ValidationError] if query data is invalid
|
85
|
+
# @raise [StorageError] if save operation fails
|
86
|
+
def save!(query)
|
87
|
+
with_error_handling("save query") do
|
88
|
+
save(query) or raise StorageError, "Failed to save query"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Finds all queries for a specific date
|
93
|
+
#
|
94
|
+
# @param date [Date, String] the date to load queries for
|
95
|
+
# @return [Array<Hash>] array of query data for the specified date
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# queries = storage.find_by_date(Date.current)
|
99
|
+
# queries.each { |q| puts q[:sql] }
|
100
|
+
def find_by_date(date)
|
101
|
+
query_file = date_file_path(@queries_path, date)
|
102
|
+
safe_read_json(query_file)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Finds queries within a date range
|
106
|
+
#
|
107
|
+
# @param date_range [Range] range of dates to search
|
108
|
+
# @return [Array<Hash>] array of query data within the date range
|
109
|
+
#
|
110
|
+
# @example
|
111
|
+
# queries = storage.find_by_date_range(1.week.ago..Time.current)
|
112
|
+
def find_by_date_range(date_range)
|
113
|
+
date_range.flat_map do |date|
|
114
|
+
find_by_date(date)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Finds recent queries across all dates
|
119
|
+
#
|
120
|
+
# @param limit [Integer] maximum number of queries to return
|
121
|
+
# @return [Array<Hash>] array of recent query data
|
122
|
+
def recent(limit: 100)
|
123
|
+
all_queries = []
|
124
|
+
dates_descending.each do |date|
|
125
|
+
daily_queries = find_by_date(date)
|
126
|
+
all_queries.concat(daily_queries)
|
127
|
+
break if all_queries.size >= limit
|
128
|
+
end
|
129
|
+
|
130
|
+
all_queries
|
131
|
+
.sort_by { |q| q[:timestamp] }
|
132
|
+
.reverse
|
133
|
+
.first(limit)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Counts total number of queries
|
137
|
+
#
|
138
|
+
# @return [Integer] total number of stored queries
|
139
|
+
def count
|
140
|
+
query_files.sum do |file|
|
141
|
+
safe_read_json(file).size
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Counts queries for a specific date
|
146
|
+
#
|
147
|
+
# @param date [Date, String] date to count queries for
|
148
|
+
# @return [Integer] number of queries for the date
|
149
|
+
def count_by_date(date)
|
150
|
+
find_by_date(date).size
|
151
|
+
end
|
152
|
+
|
153
|
+
# Loads all queries for a specific date (legacy method)
|
154
|
+
#
|
155
|
+
# @deprecated Use {#find_by_date} instead
|
156
|
+
# @param date [Date, String] the date to load queries for
|
157
|
+
# @return [Array<Hash>] array of query data
|
158
|
+
def load_for_date(date)
|
159
|
+
find_by_date(date)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Removes old query files based on retention period
|
163
|
+
#
|
164
|
+
# @param days_to_keep [Integer] number of days of queries to retain
|
165
|
+
# @return [Integer] number of files removed
|
166
|
+
def cleanup_old_queries(days_to_keep = DEFAULT_CLEANUP_DAYS)
|
167
|
+
cutoff_date = cleanup_cutoff_date(days_to_keep)
|
168
|
+
removed_count = 0
|
169
|
+
|
170
|
+
cleanup_files_older_than(cutoff_date) do
|
171
|
+
removed_count += 1
|
172
|
+
end
|
173
|
+
|
174
|
+
removed_count
|
175
|
+
end
|
176
|
+
|
177
|
+
# Optimizes storage by removing duplicate queries
|
178
|
+
#
|
179
|
+
# @return [Integer] number of duplicates removed
|
180
|
+
def optimize_storage
|
181
|
+
duplicate_count = 0
|
182
|
+
|
183
|
+
query_files.each do |file|
|
184
|
+
queries = safe_read_json(file)
|
185
|
+
unique_queries = queries.uniq { |q| [q[:sql], q[:timestamp]] }
|
186
|
+
|
187
|
+
if unique_queries.size < queries.size
|
188
|
+
duplicate_count += queries.size - unique_queries.size
|
189
|
+
safe_write_json(file, unique_queries)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
duplicate_count
|
194
|
+
end
|
195
|
+
|
196
|
+
# Clears all query logs
|
197
|
+
#
|
198
|
+
# @return [Integer] number of files removed
|
199
|
+
def clear_all
|
200
|
+
with_error_handling("clear all queries") do
|
201
|
+
# Count files before deleting
|
202
|
+
file_count = count_query_files
|
203
|
+
|
204
|
+
safe_delete_directory(queries_path)
|
205
|
+
|
206
|
+
file_count
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Counts the number of query files
|
211
|
+
#
|
212
|
+
# @return [Integer] number of query files
|
213
|
+
def count_query_files
|
214
|
+
return 0 unless Dir.exist?(@queries_path)
|
215
|
+
|
216
|
+
query_files.count
|
217
|
+
end
|
218
|
+
|
219
|
+
private
|
220
|
+
|
221
|
+
# Normalizes query data to hash format
|
222
|
+
#
|
223
|
+
# @param query [Hash, Object] query object or hash
|
224
|
+
# @return [Hash] normalized query data
|
225
|
+
def normalize_query_data(query)
|
226
|
+
case query
|
227
|
+
when Hash
|
228
|
+
normalize_hash_keys(query)
|
229
|
+
when ->(q) { q.respond_to?(:to_h) }
|
230
|
+
normalize_hash_keys(query.to_h)
|
231
|
+
else
|
232
|
+
raise ValidationError, "Query must be a Hash or respond to :to_h"
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Normalizes hash keys to symbols (Rails-compatible)
|
237
|
+
#
|
238
|
+
# @param hash [Hash] hash to normalize
|
239
|
+
# @return [Hash] hash with symbolized keys
|
240
|
+
def normalize_hash_keys(hash)
|
241
|
+
if hash.respond_to?(:with_indifferent_access)
|
242
|
+
hash.with_indifferent_access
|
243
|
+
else
|
244
|
+
hash.transform_keys(&:to_sym)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Loads existing queries from a file
|
249
|
+
#
|
250
|
+
# @param query_file [String] path to the query file
|
251
|
+
# @return [Array<Hash>] existing queries or empty array
|
252
|
+
def load_queries_from_file(query_file)
|
253
|
+
safe_read_json(query_file)
|
254
|
+
end
|
255
|
+
|
256
|
+
# Adds a new query and applies daily limits
|
257
|
+
#
|
258
|
+
# @param queries [Array<Hash>] existing queries
|
259
|
+
# @param new_query [Hash] new query to add
|
260
|
+
# @return [Array<Hash>] updated queries with limits applied
|
261
|
+
def add_query_with_limits(queries, new_query)
|
262
|
+
updated_queries = queries + [new_query]
|
263
|
+
apply_daily_limits(updated_queries)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Applies daily query limits based on configuration
|
267
|
+
#
|
268
|
+
# @param queries [Array<Hash>] queries to limit
|
269
|
+
# @return [Array<Hash>] limited queries (keeps most recent)
|
270
|
+
def apply_daily_limits(queries)
|
271
|
+
max_queries = Dbwatcher.configuration.max_query_logs_per_day || MAX_QUERIES_PER_FILE
|
272
|
+
return queries if max_queries <= 0
|
273
|
+
|
274
|
+
queries
|
275
|
+
.sort_by { |q| normalize_timestamp_for_sorting(q[:timestamp]) }
|
276
|
+
.last(max_queries)
|
277
|
+
end
|
278
|
+
|
279
|
+
# Removes query files older than the cutoff date
|
280
|
+
#
|
281
|
+
# @param cutoff_date [Time] files older than this date are removed
|
282
|
+
# @yield [String] called for each file removed
|
283
|
+
# @return [void]
|
284
|
+
def cleanup_files_older_than(cutoff_date)
|
285
|
+
safe_operation("cleanup old queries") do
|
286
|
+
query_files.each do |file|
|
287
|
+
if File.mtime(file) < cutoff_date
|
288
|
+
file_manager.delete_file(file)
|
289
|
+
yield file if block_given?
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Returns all query files
|
296
|
+
#
|
297
|
+
# @return [Array<String>] paths to all query files
|
298
|
+
def query_files
|
299
|
+
file_manager.glob_files(File.join(@queries_path, "*.json"))
|
300
|
+
end
|
301
|
+
|
302
|
+
# Normalizes timestamp for sorting to handle mixed string/Time types
|
303
|
+
#
|
304
|
+
# @param timestamp [String, Time, nil] timestamp to normalize
|
305
|
+
# @return [Time] normalized timestamp
|
306
|
+
def normalize_timestamp_for_sorting(timestamp)
|
307
|
+
case timestamp
|
308
|
+
when Time
|
309
|
+
timestamp
|
310
|
+
when String
|
311
|
+
Time.parse(timestamp)
|
312
|
+
else
|
313
|
+
Time.at(0) # Fallback for nil or invalid timestamps
|
314
|
+
end
|
315
|
+
rescue ArgumentError
|
316
|
+
Time.at(0) # Fallback for unparseable strings
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns dates in descending order based on existing files
|
320
|
+
#
|
321
|
+
# @return [Array<Date>] sorted dates
|
322
|
+
def dates_descending
|
323
|
+
query_files
|
324
|
+
.map { |file| File.basename(file, ".json") }
|
325
|
+
.map do |filename|
|
326
|
+
Date.parse(filename)
|
327
|
+
rescue StandardError
|
328
|
+
nil
|
329
|
+
end
|
330
|
+
.compact
|
331
|
+
.sort
|
332
|
+
.reverse
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
class QueryValidator
|
6
|
+
REQUIRED_FIELDS = [:timestamp].freeze
|
7
|
+
|
8
|
+
def self.valid?(query)
|
9
|
+
return false unless query.is_a?(Hash)
|
10
|
+
|
11
|
+
REQUIRED_FIELDS.all? { |field| query.key?(field) && !query[field].nil? }
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.validate!(query)
|
15
|
+
raise ArgumentError, "Query must be a Hash" unless query.is_a?(Hash)
|
16
|
+
|
17
|
+
missing_fields = REQUIRED_FIELDS.reject { |field| query.key?(field) }
|
18
|
+
return true if missing_fields.empty?
|
19
|
+
|
20
|
+
raise ArgumentError, "Missing required fields: #{missing_fields.join(", ")}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
class Session
|
6
|
+
attr_accessor :id, :name, :metadata, :started_at, :ended_at, :changes
|
7
|
+
|
8
|
+
def initialize(attrs = {})
|
9
|
+
# Set default values
|
10
|
+
@changes = []
|
11
|
+
@metadata = {}
|
12
|
+
|
13
|
+
# Set provided attributes
|
14
|
+
attrs.each do |key, value|
|
15
|
+
setter_method = "#{key}="
|
16
|
+
send(setter_method, value) if respond_to?(setter_method)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{
|
22
|
+
id: id,
|
23
|
+
name: name,
|
24
|
+
metadata: metadata,
|
25
|
+
started_at: started_at,
|
26
|
+
ended_at: ended_at,
|
27
|
+
changes: changes
|
28
|
+
}
|
29
|
+
end
|
30
|
+
|
31
|
+
def summary
|
32
|
+
return {} unless changes.is_a?(Array)
|
33
|
+
|
34
|
+
valid_changes = filter_valid_changes
|
35
|
+
group_changes_by_operation(valid_changes)
|
36
|
+
rescue StandardError => e
|
37
|
+
warn "Failed to calculate session summary: #{e.message}"
|
38
|
+
{}
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def filter_valid_changes
|
44
|
+
changes.select { |change| valid_change?(change) }
|
45
|
+
end
|
46
|
+
|
47
|
+
def valid_change?(change)
|
48
|
+
change.is_a?(Hash) && change[:table_name] && change[:operation]
|
49
|
+
end
|
50
|
+
|
51
|
+
def group_changes_by_operation(valid_changes)
|
52
|
+
valid_changes
|
53
|
+
.group_by { |change| "#{change[:table_name]},#{change[:operation]}" }
|
54
|
+
.transform_values(&:count)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module Storage
|
5
|
+
class SessionOperations
|
6
|
+
# Include data normalization capabilities
|
7
|
+
include Concerns::DataNormalizer
|
8
|
+
def initialize(sessions_path, index_file)
|
9
|
+
@sessions_path = sessions_path
|
10
|
+
@index_file = index_file
|
11
|
+
end
|
12
|
+
|
13
|
+
def session_file_path(session_id)
|
14
|
+
File.join(@sessions_path, "#{session_id}.json")
|
15
|
+
end
|
16
|
+
|
17
|
+
def build_session_summary(session)
|
18
|
+
session_data = normalize_session_data(session)
|
19
|
+
|
20
|
+
{
|
21
|
+
id: session_data[:id],
|
22
|
+
name: session_data[:name],
|
23
|
+
started_at: session_data[:started_at],
|
24
|
+
ended_at: session_data[:ended_at],
|
25
|
+
change_count: (session_data[:changes] || []).count
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply_session_limits(sessions)
|
30
|
+
max_sessions = Dbwatcher.configuration.max_sessions
|
31
|
+
return sessions unless max_sessions&.positive?
|
32
|
+
|
33
|
+
sessions.first(max_sessions)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "time"
|
4
|
+
|
5
|
+
module Dbwatcher
|
6
|
+
module Storage
|
7
|
+
class SessionQuery
|
8
|
+
def initialize(storage)
|
9
|
+
@storage = storage
|
10
|
+
@conditions = {}
|
11
|
+
@limit_value = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def find(id)
|
15
|
+
@storage.load(id)
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
apply_filters(@storage.all)
|
20
|
+
end
|
21
|
+
|
22
|
+
def where(conditions)
|
23
|
+
@conditions.merge!(conditions)
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def limit(count)
|
28
|
+
@limit_value = count
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def recent(days: 7)
|
33
|
+
cutoff = Time.now - (days * 24 * 60 * 60)
|
34
|
+
where(started_after: cutoff)
|
35
|
+
end
|
36
|
+
|
37
|
+
def create(session)
|
38
|
+
@storage.save(session)
|
39
|
+
end
|
40
|
+
|
41
|
+
def with_changes
|
42
|
+
all.select { |s| @storage.load(s[:id])&.changes&.any? }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def apply_filters(sessions)
|
48
|
+
result = sessions
|
49
|
+
result = result.select { |s| matches_conditions?(s) } if @conditions.any?
|
50
|
+
result = result.first(@limit_value) if @limit_value
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def matches_conditions?(session)
|
55
|
+
@conditions.all? do |key, value|
|
56
|
+
case key
|
57
|
+
when :started_after
|
58
|
+
started_at = session[:started_at]
|
59
|
+
started_at && Time.parse(started_at) >= value
|
60
|
+
when :name
|
61
|
+
session[:name]&.include?(value)
|
62
|
+
else
|
63
|
+
session[key] == value
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue ArgumentError, TypeError
|
67
|
+
false
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|