timet 1.6.2 → 1.6.3
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/.reek.yml +17 -1
- data/.rubocop.yml +4 -0
- data/CHANGELOG.md +19 -2
- data/README.md +1 -0
- data/lib/timet/application.rb +3 -3
- data/lib/timet/database.rb +25 -25
- data/lib/timet/database_syncer.rb +93 -131
- data/lib/timet/s3_supabase.rb +74 -131
- data/lib/timet/table.rb +0 -4
- data/lib/timet/tag_distribution.rb +181 -160
- data/lib/timet/time_report.rb +17 -3
- data/lib/timet/validation_editor.rb +303 -0
- data/lib/timet/version.rb +2 -2
- metadata +3 -7
- data/lib/timet/item_data_helper.rb +0 -59
- data/lib/timet/time_report_helper.rb +0 -36
- data/lib/timet/time_update_helper.rb +0 -75
- data/lib/timet/time_validation_helper.rb +0 -175
- data/lib/timet/validation_edit_helper.rb +0 -160
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 84fa974afe30946a8b292569e74917c3992097e0ae652339320981f2951f298d
|
|
4
|
+
data.tar.gz: dc6cf2d3f5bd30feebb93db4aa07c436203e53bf1d9d0dbba5166695b622b021
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49cf7cc38f9e44687d27bee93a8de5678963fd92a9589241e38635afcdf4b86f8229d1c401483d2d6e955ee83e5423ca10f832f4beb0101873e60f5d0ff34821
|
|
7
|
+
data.tar.gz: 2098f1a3c297277bd07425961b3e84425a1189ae5984e21c35649a3a2c517a5a175dfd5b477abb59ba49f78925a9c4ec872006d3be025ddc4ee363bf86ff7004
|
data/.reek.yml
CHANGED
|
@@ -3,4 +3,20 @@ detectors:
|
|
|
3
3
|
TooManyStatements:
|
|
4
4
|
max_statements: 7
|
|
5
5
|
UncommunicativeVariableName:
|
|
6
|
-
enabled: false
|
|
6
|
+
enabled: false
|
|
7
|
+
FeatureEnvy:
|
|
8
|
+
exclude:
|
|
9
|
+
- fetch_last_id
|
|
10
|
+
- last_item
|
|
11
|
+
- update_time_columns
|
|
12
|
+
TooManyMethods:
|
|
13
|
+
max_methods: 20
|
|
14
|
+
exclude:
|
|
15
|
+
- ValidationEditor
|
|
16
|
+
LongParameterList:
|
|
17
|
+
exclude:
|
|
18
|
+
- process_and_update_time_field
|
|
19
|
+
- invalid_time_value?
|
|
20
|
+
DataClump:
|
|
21
|
+
exclude:
|
|
22
|
+
- TimeValidationHelpers
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [1.6.3] - 2026-02-28
|
|
2
|
+
|
|
3
|
+
**Improvements:**
|
|
4
|
+
|
|
5
|
+
- **Validation Architecture:** Refactored validation logic from `ValidationEditHelper` module to `ValidationEditor` class for better encapsulation and testability.
|
|
6
|
+
- **Synchronization & Storage:** Simplified `DatabaseSyncer` and `S3Supabase` by extracting modular helper methods, introducing `S3Config` module, and `S3ObjectRef` struct.
|
|
7
|
+
- **Reporting Features:** Added `export_csv` and `export_icalendar` methods to `TimeReport` for enhanced data portability.
|
|
8
|
+
- **Database Refactoring:** Converted `Timet::Database` methods to class methods where appropriate, extracted column checks, and simplified item fetching logic.
|
|
9
|
+
- **Tag Distribution:** Split `TagDistribution` into dedicated formatting and logic modules using a Context struct for clearer rendering.
|
|
10
|
+
- **Dependencies:** Updated `aws-sdk-s3` to `~> 1.213`, removed unused `httparty` gem, and bumped RuboCop, parser, and thor dependencies.
|
|
11
|
+
- **Code Quality:** Updated `.reek.yml` and RuboCop configs to accommodate new method structures, exclusions, and added code coverage badge to README.
|
|
12
|
+
- **Cleanup:** Removed obsolete helper modules (`item_data_helper`, `time_report_helper`, `time_update_helper`, `time_validation_helper`).
|
|
13
|
+
|
|
14
|
+
**Bug Fixes:**
|
|
15
|
+
|
|
16
|
+
- **Status Determination:** Added nil guards to `determine_status` to prevent runtime errors during item status checks.
|
|
17
|
+
- **Data Integrity:** Resolved duplicated `get_item_values` logic in syncer to ensure consistency during inserts and updates.
|
|
18
|
+
- **Validation Logic:** Updated validation methods to correctly handle end datetime adjustments for the next day.
|
|
19
|
+
|
|
1
20
|
## [1.6.2] - 2025-12-28
|
|
2
21
|
|
|
3
22
|
**Improvements:**
|
|
@@ -440,7 +459,6 @@
|
|
|
440
459
|
**Improvements:**
|
|
441
460
|
|
|
442
461
|
- **Refactor `TimeReport` to use `TimeReportHelper` module for utility methods:**
|
|
443
|
-
|
|
444
462
|
- Extracted utility methods (`add_hashes`, `date_ranges`, `format_item`, `valid_date_format?`) into a new `TimeReportHelper` module.
|
|
445
463
|
- Updated `TimeReport` class to include `TimeReportHelper` module.
|
|
446
464
|
- Removed redundant utility methods from `TimeReport` class.
|
|
@@ -450,7 +468,6 @@
|
|
|
450
468
|
- Adjusted formatting in `total` method for better alignment.
|
|
451
469
|
|
|
452
470
|
- **Refactor `Timet::Formatter` to improve readability and modularity:**
|
|
453
|
-
|
|
454
471
|
- Introduced a constant `CHAR_MAPPING` to store block characters for different value ranges.
|
|
455
472
|
- Refactored `format_notes` method to use a more descriptive variable name for the maximum length.
|
|
456
473
|
- Updated `format_tag_distribution` method to accept `colors` parameter and pass it to `process_and_print_tags`.
|
data/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[](https://badge.fury.io/rb/timet)
|
|
2
2
|

|
|
3
3
|
[](https://qlty.sh/gh/frankvielma/projects/timet)
|
|
4
|
+
[](https://qlty.sh/gh/frankvielma/projects/timet)
|
|
4
5
|
|
|
5
6
|
# Timet
|
|
6
7
|
|
data/lib/timet/application.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'thor'
|
|
|
4
4
|
require 'tty-prompt'
|
|
5
5
|
require 'icalendar'
|
|
6
6
|
require_relative 's3_supabase'
|
|
7
|
-
require_relative '
|
|
7
|
+
require_relative 'validation_editor'
|
|
8
8
|
require_relative 'application_helper'
|
|
9
9
|
require_relative 'time_helper'
|
|
10
10
|
require_relative 'version'
|
|
@@ -22,7 +22,6 @@ module Timet
|
|
|
22
22
|
# - delete: Delete a task
|
|
23
23
|
# - cancel: Cancel active time tracking
|
|
24
24
|
class Application < Thor
|
|
25
|
-
include ValidationEditHelper
|
|
26
25
|
include ApplicationHelper
|
|
27
26
|
include TimeHelper
|
|
28
27
|
|
|
@@ -271,7 +270,8 @@ Update start time => tt edit 12 start 12:33'
|
|
|
271
270
|
new_value = prompt_for_new_value(item, field)
|
|
272
271
|
end
|
|
273
272
|
|
|
274
|
-
|
|
273
|
+
editor = Timet::ValidationEditor.new(item, @db)
|
|
274
|
+
updated_item = editor.update(field, new_value)
|
|
275
275
|
@db.update_item(id, field, updated_item[FIELD_INDEX[field]])
|
|
276
276
|
display_item(updated_item)
|
|
277
277
|
end
|
data/lib/timet/database.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Timet
|
|
|
24
24
|
# @note The method creates a new SQLite3 database connection and initializes the necessary tables if they
|
|
25
25
|
# do not already exist.
|
|
26
26
|
def initialize(database_path = DEFAULT_DATABASE_PATH)
|
|
27
|
-
move_old_database_file(database_path)
|
|
27
|
+
self.class.move_old_database_file(database_path)
|
|
28
28
|
|
|
29
29
|
@db = SQLite3::Database.new(database_path)
|
|
30
30
|
create_table
|
|
@@ -76,15 +76,17 @@ module Timet
|
|
|
76
76
|
raise 'Invalid table name' unless table_name == 'items'
|
|
77
77
|
raise 'Invalid column name' unless /\A[a-zA-Z0-9_]+\z/.match?(new_column_name)
|
|
78
78
|
raise 'Invalid date type' unless %w[INTEGER TEXT BOOLEAN].include?(date_type)
|
|
79
|
-
|
|
80
|
-
result = execute_sql("SELECT count(*) FROM pragma_table_info('items') where name=?", [new_column_name])
|
|
81
|
-
column_exists = result[0][0].positive?
|
|
82
|
-
return if column_exists
|
|
79
|
+
return if column_exists?(new_column_name)
|
|
83
80
|
|
|
84
81
|
execute_sql("ALTER TABLE #{table_name} ADD COLUMN #{new_column_name} #{date_type}")
|
|
85
82
|
puts "Column '#{new_column_name}' added to table '#{table_name}'."
|
|
86
83
|
end
|
|
87
84
|
|
|
85
|
+
def column_exists?(new_column_name)
|
|
86
|
+
execute_sql("SELECT count(*) FROM pragma_table_info('items') where name=?",
|
|
87
|
+
[new_column_name]).first.first.positive?
|
|
88
|
+
end
|
|
89
|
+
|
|
88
90
|
# Inserts a new item into the items table.
|
|
89
91
|
#
|
|
90
92
|
# @param start [Integer] The start time of the item.
|
|
@@ -150,8 +152,8 @@ module Timet
|
|
|
150
152
|
#
|
|
151
153
|
# @note The method executes SQL to fetch the ID of the last inserted item.
|
|
152
154
|
def fetch_last_id
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
execute_sql('SELECT id FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
|
|
156
|
+
.then { |result| result.empty? ? nil : result.first.first }
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
# Fetches the last item from the items table.
|
|
@@ -163,8 +165,8 @@ module Timet
|
|
|
163
165
|
#
|
|
164
166
|
# @note The method executes SQL to fetch the last item from the 'items' table.
|
|
165
167
|
def last_item
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
execute_sql('SELECT * FROM items WHERE deleted IS NULL OR deleted = 0 ORDER BY id DESC LIMIT 1')
|
|
169
|
+
.then { |result| result.empty? ? nil : result.first }
|
|
168
170
|
end
|
|
169
171
|
|
|
170
172
|
# Finds an item by its ID.
|
|
@@ -180,8 +182,7 @@ module Timet
|
|
|
180
182
|
# @note If the item is found, it returns the item as an array.
|
|
181
183
|
# @note If the item is not found, it returns nil.
|
|
182
184
|
def find_item(id)
|
|
183
|
-
|
|
184
|
-
result.first.dup if result.any? # Add .dup to create a copy
|
|
185
|
+
execute_sql('SELECT * FROM items WHERE id = ?', [id]).first&.dup
|
|
185
186
|
end
|
|
186
187
|
|
|
187
188
|
# Fetches all items from the items table that have a start time greater than or equal to today.
|
|
@@ -212,8 +213,8 @@ module Timet
|
|
|
212
213
|
#
|
|
213
214
|
# @see StatusHelper#determine_status
|
|
214
215
|
def item_status(id = nil)
|
|
215
|
-
id
|
|
216
|
-
determine_status(find_item(id))
|
|
216
|
+
id ||= fetch_last_id
|
|
217
|
+
self.class.determine_status(find_item(id))
|
|
217
218
|
end
|
|
218
219
|
|
|
219
220
|
# Executes a SQL query and returns the result.
|
|
@@ -281,8 +282,8 @@ module Timet
|
|
|
281
282
|
# @note The method checks if the result set is empty and returns :no_items if true.
|
|
282
283
|
# @note If the last item in the result set has no end time, it returns :in_progress.
|
|
283
284
|
# @note If the last item in the result set has an end time, it returns :complete.
|
|
284
|
-
def determine_status(result)
|
|
285
|
-
return :no_items
|
|
285
|
+
def self.determine_status(result)
|
|
286
|
+
return :no_items unless result
|
|
286
287
|
|
|
287
288
|
last_item_end = result[2]
|
|
288
289
|
return :in_progress unless last_item_end
|
|
@@ -293,11 +294,12 @@ module Timet
|
|
|
293
294
|
# Moves the old database file to the new location if it exists.
|
|
294
295
|
#
|
|
295
296
|
# @param database_path [String] The path to the new SQLite database file.
|
|
296
|
-
def move_old_database_file(database_path)
|
|
297
|
+
def self.move_old_database_file(database_path)
|
|
297
298
|
old_file = File.join(Dir.home, '.timet.db')
|
|
298
299
|
return unless File.exist?(old_file)
|
|
299
300
|
|
|
300
|
-
|
|
301
|
+
dir = File.dirname(database_path)
|
|
302
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
301
303
|
FileUtils.mv(old_file, database_path)
|
|
302
304
|
end
|
|
303
305
|
|
|
@@ -317,14 +319,12 @@ module Timet
|
|
|
317
319
|
# @raise [StandardError] If there is an issue executing the SQL queries, an error may be raised.
|
|
318
320
|
#
|
|
319
321
|
def update_time_columns
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
execute_sql('UPDATE items SET updated_at = ?, created_at = ? WHERE id = ?', [fallback_time, fallback_time, id])
|
|
327
|
-
end
|
|
322
|
+
execute_sql('SELECT * FROM items WHERE updated_at IS NULL OR created_at IS NULL')
|
|
323
|
+
.each { |item| update_timestamp_for_item(item[0], item[2] || item[1] || Time.now.to_i) }
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def update_timestamp_for_item(id, fallback_time)
|
|
327
|
+
execute_sql('UPDATE items SET updated_at = ?, created_at = ? WHERE id = ?', [fallback_time, fallback_time, id])
|
|
328
328
|
end
|
|
329
329
|
end
|
|
330
330
|
end
|
|
@@ -3,51 +3,41 @@
|
|
|
3
3
|
module Timet
|
|
4
4
|
# Module responsible for synchronizing local and remote databases
|
|
5
5
|
module DatabaseSyncer
|
|
6
|
-
# Fields used in item operations
|
|
7
6
|
ITEM_FIELDS = %w[start end tag notes pomodoro updated_at created_at deleted].freeze
|
|
8
7
|
|
|
9
|
-
# Handles the synchronization process when differences are detected between databases
|
|
10
|
-
#
|
|
11
|
-
# @param local_db [SQLite3::Database] The local database connection
|
|
12
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
|
13
|
-
# @param bucket [String] The S3 bucket name
|
|
14
|
-
# @param local_db_path [String] Path to the local database file
|
|
15
|
-
# @param remote_path [String] Path to the downloaded remote database file
|
|
16
|
-
# @return [void]
|
|
17
|
-
# @note This method attempts to sync the databases and handles any errors that occur during the process
|
|
18
8
|
def handle_database_differences(*args)
|
|
19
9
|
local_db, remote_storage, bucket, local_db_path, remote_path = args
|
|
20
10
|
puts 'Differences detected between local and remote databases'
|
|
21
11
|
begin
|
|
22
12
|
sync_with_remote_database(local_db, remote_path, remote_storage, bucket, local_db_path)
|
|
23
13
|
rescue SQLite3::Exception => e
|
|
24
|
-
handle_sync_error(e, remote_storage, bucket, local_db_path)
|
|
14
|
+
handle_sync_error(e, remote_storage: remote_storage, bucket: bucket, local_db_path: local_db_path)
|
|
25
15
|
end
|
|
26
16
|
end
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
18
|
+
def handle_sync_error(error, *args)
|
|
19
|
+
first_arg = args.first
|
|
20
|
+
if first_arg.is_a?(Hash)
|
|
21
|
+
options = first_arg
|
|
22
|
+
remote_storage, bucket, local_db_path = options.values_at(:remote_storage, :bucket, :local_db_path)
|
|
23
|
+
else
|
|
24
|
+
remote_storage, bucket, local_db_path = args
|
|
25
|
+
end
|
|
26
|
+
report_sync_error(error)
|
|
27
|
+
upload_local_database(remote_storage, bucket, local_db_path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def report_sync_error(error)
|
|
37
31
|
puts "Error opening remote database: #{error.message}"
|
|
38
32
|
puts 'Uploading local database to replace corrupted remote database'
|
|
33
|
+
end
|
|
34
|
+
module_function :report_sync_error
|
|
35
|
+
|
|
36
|
+
def upload_local_database(remote_storage, bucket, local_db_path)
|
|
39
37
|
remote_storage.upload_file(bucket, local_db_path, 'timet.db')
|
|
40
38
|
end
|
|
39
|
+
module_function :upload_local_database
|
|
41
40
|
|
|
42
|
-
# Performs the actual database synchronization by setting up connections and syncing data
|
|
43
|
-
#
|
|
44
|
-
# @param local_db [SQLite3::Database] The local database connection
|
|
45
|
-
# @param remote_path [String] Path to the remote database file
|
|
46
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
|
47
|
-
# @param bucket [String] The S3 bucket name
|
|
48
|
-
# @param local_db_path [String] Path to the local database file
|
|
49
|
-
# @return [void]
|
|
50
|
-
# @note Configures both databases to return results as hashes for consistent data handling
|
|
51
41
|
def sync_with_remote_database(*args)
|
|
52
42
|
local_db, remote_path, remote_storage, bucket, local_db_path = args
|
|
53
43
|
db_remote = open_remote_database(remote_path)
|
|
@@ -56,12 +46,6 @@ module Timet
|
|
|
56
46
|
sync_databases(local_db, db_remote, remote_storage, bucket, local_db_path)
|
|
57
47
|
end
|
|
58
48
|
|
|
59
|
-
# Opens and validates a connection to the remote database
|
|
60
|
-
#
|
|
61
|
-
# @param remote_path [String] Path to the remote database file
|
|
62
|
-
# @return [SQLite3::Database] The initialized database connection
|
|
63
|
-
# @raise [RuntimeError] If the database connection cannot be established
|
|
64
|
-
# @note Validates that the database connection is properly initialized
|
|
65
49
|
def open_remote_database(remote_path)
|
|
66
50
|
db_remote = SQLite3::Database.new(remote_path)
|
|
67
51
|
raise 'Failed to initialize remote database' unless db_remote
|
|
@@ -69,15 +53,6 @@ module Timet
|
|
|
69
53
|
db_remote
|
|
70
54
|
end
|
|
71
55
|
|
|
72
|
-
# Synchronizes the local and remote databases by comparing and merging their items
|
|
73
|
-
#
|
|
74
|
-
# @param local_db [SQLite3::Database] The local database connection
|
|
75
|
-
# @param remote_db [SQLite3::Database] The remote database connection
|
|
76
|
-
# @param remote_storage [S3Supabase] The remote storage client for cloud operations
|
|
77
|
-
# @param bucket [String] The S3 bucket name
|
|
78
|
-
# @param local_db_path [String] Path to the local database file
|
|
79
|
-
# @return [void]
|
|
80
|
-
# @note This method orchestrates the entire database synchronization process
|
|
81
56
|
def sync_databases(*args)
|
|
82
57
|
local_db, remote_db, remote_storage, bucket, local_db_path = args
|
|
83
58
|
process_database_items(local_db, remote_db)
|
|
@@ -85,130 +60,117 @@ module Timet
|
|
|
85
60
|
puts 'Database sync completed'
|
|
86
61
|
end
|
|
87
62
|
|
|
88
|
-
# Processes items from both databases and syncs them
|
|
89
|
-
#
|
|
90
|
-
# @param local_db [SQLite3::Database] The local database connection
|
|
91
|
-
# @param remote_db [SQLite3::Database] The remote database connection
|
|
92
|
-
# @return [void]
|
|
93
63
|
def process_database_items(local_db, remote_db)
|
|
94
64
|
remote_items = remote_db.execute('SELECT * FROM items ORDER BY updated_at DESC')
|
|
95
65
|
local_items = local_db.execute_sql('SELECT * FROM items ORDER BY updated_at DESC')
|
|
96
66
|
|
|
97
|
-
sync_items_by_id(
|
|
98
|
-
local_db,
|
|
99
|
-
items_to_hash(local_items),
|
|
100
|
-
items_to_hash(remote_items)
|
|
101
|
-
)
|
|
67
|
+
sync_items_by_id(local_db, items_to_hash(local_items), items_to_hash(remote_items))
|
|
102
68
|
end
|
|
103
69
|
|
|
104
|
-
# Syncs items between local and remote databases based on their IDs
|
|
105
|
-
#
|
|
106
|
-
# @param local_db [SQLite3::Database] The local database connection
|
|
107
|
-
# @param local_items_by_id [Hash] Local items indexed by ID
|
|
108
|
-
# @param remote_items_by_id [Hash] Remote items indexed by ID
|
|
109
|
-
# @return [void]
|
|
110
70
|
def sync_items_by_id(local_db, local_items_by_id, remote_items_by_id)
|
|
111
71
|
all_item_ids = (remote_items_by_id.keys + local_items_by_id.keys).uniq
|
|
72
|
+
all_item_ids.each { |id| sync_single_item(local_db, id, local_items_by_id, remote_items_by_id) }
|
|
73
|
+
end
|
|
112
74
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
75
|
+
def sync_single_item(*args)
|
|
76
|
+
local_db, id, local_items_by_id, remote_items_by_id = args
|
|
77
|
+
remote_item = remote_items_by_id[id]
|
|
78
|
+
local_item = local_items_by_id[id]
|
|
79
|
+
|
|
80
|
+
if !remote_item
|
|
81
|
+
log_local_only(id)
|
|
82
|
+
elsif !local_item
|
|
83
|
+
add_remote_item(local_db, id, remote_item)
|
|
84
|
+
else
|
|
85
|
+
merge_item(local_db, id, local_item, remote_item)
|
|
122
86
|
end
|
|
123
87
|
end
|
|
124
88
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
89
|
+
def log_local_only(id)
|
|
90
|
+
puts "Local item #{id} will be uploaded"
|
|
91
|
+
end
|
|
92
|
+
module_function :log_local_only
|
|
93
|
+
|
|
94
|
+
def add_remote_item(local_db, id, remote_item)
|
|
95
|
+
puts "Adding remote item #{id} to local"
|
|
96
|
+
insert_item_from_hash(local_db, remote_item)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def process_existing_item(id, local_item, remote_item, local_db)
|
|
100
|
+
merge_item(local_db, id, local_item, remote_item)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def merge_item(*args)
|
|
104
|
+
local_db, id, local_item, remote_item = args
|
|
105
|
+
local_time = extract_timestamp(local_item)
|
|
106
|
+
remote_time = extract_timestamp(remote_item)
|
|
107
|
+
|
|
108
|
+
return resolve_remote_wins(local_db, id, remote_item) if remote_wins?(remote_item, remote_time, local_time)
|
|
109
|
+
|
|
110
|
+
log_local_wins(id, local_item)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def resolve_remote_wins(local_db, id, remote_item)
|
|
114
|
+
puts format_status_message(id, remote_item, 'Remote')
|
|
115
|
+
update_item_from_hash(local_db, remote_item)
|
|
116
|
+
:local_update
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def log_local_wins(id, local_item)
|
|
120
|
+
puts format_status_message(id, local_item, 'Local')
|
|
121
|
+
:remote_update
|
|
122
|
+
end
|
|
123
|
+
|
|
130
124
|
def insert_item_from_hash(db, item)
|
|
131
125
|
fields = ['id', *ITEM_FIELDS].join(', ')
|
|
132
126
|
placeholders = Array.new(ITEM_FIELDS.length + 1, '?').join(', ')
|
|
133
127
|
db.execute_sql(
|
|
134
128
|
"INSERT INTO items (#{fields}) VALUES (#{placeholders})",
|
|
135
|
-
|
|
129
|
+
get_insert_values(item)
|
|
136
130
|
)
|
|
137
131
|
end
|
|
138
132
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
remote_time = remote_item['updated_at'].to_i
|
|
150
|
-
|
|
151
|
-
if remote_wins?(remote_item, remote_time, local_time)
|
|
152
|
-
puts format_status_message(id, remote_item, 'Remote')
|
|
153
|
-
update_item_from_hash(local_db, remote_item)
|
|
154
|
-
:local_update
|
|
155
|
-
elsif local_time > remote_time
|
|
156
|
-
puts format_status_message(id, local_item, 'Local')
|
|
157
|
-
:remote_update
|
|
158
|
-
end
|
|
133
|
+
def update_item_from_hash(db, item)
|
|
134
|
+
fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
|
|
135
|
+
db.execute_sql(
|
|
136
|
+
"UPDATE items SET #{fields} WHERE id = ?",
|
|
137
|
+
get_update_values(item)
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def extract_timestamp(item)
|
|
142
|
+
item['updated_at'].to_i
|
|
159
143
|
end
|
|
144
|
+
module_function :extract_timestamp
|
|
160
145
|
|
|
161
|
-
# Converts database items to a hash indexed by ID
|
|
162
|
-
#
|
|
163
|
-
# @param items [Array<Hash>] Array of database items
|
|
164
|
-
# @return [Hash] Items indexed by ID
|
|
165
146
|
def items_to_hash(items)
|
|
166
147
|
items.to_h { |item| [item['id'], item] }
|
|
167
148
|
end
|
|
168
149
|
|
|
169
|
-
# Determines if remote item should take precedence
|
|
170
|
-
#
|
|
171
|
-
# @param remote_item [Hash] Remote database item
|
|
172
|
-
# @param remote_time [Integer] Remote item timestamp
|
|
173
|
-
# @param local_time [Integer] Local item timestamp
|
|
174
|
-
# @return [Boolean] true if remote item should take precedence
|
|
175
150
|
def remote_wins?(remote_item, remote_time, local_time)
|
|
176
|
-
|
|
151
|
+
time_diff = remote_time > local_time
|
|
152
|
+
time_diff && (remote_item['deleted'].to_i == 1 || time_diff)
|
|
177
153
|
end
|
|
178
154
|
|
|
179
|
-
# Formats item status message
|
|
180
|
-
#
|
|
181
|
-
# @param id [Integer] Item ID
|
|
182
|
-
# @param item [Hash] Database item
|
|
183
|
-
# @param source [String] Source of the item ('Remote' or 'Local')
|
|
184
|
-
# @return [String] Formatted status message
|
|
185
155
|
def format_status_message(id, item, source)
|
|
186
156
|
deleted = item['deleted'].to_i == 1 ? ' and deleted' : ''
|
|
187
157
|
"#{source} item #{id} is newer#{deleted} - #{source == 'Remote' ? 'updating local' : 'will be uploaded'}"
|
|
188
158
|
end
|
|
159
|
+
module_function :format_status_message
|
|
189
160
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
# @return [void]
|
|
195
|
-
def update_item_from_hash(db, item)
|
|
196
|
-
fields = "#{ITEM_FIELDS.join(' = ?, ')} = ?"
|
|
197
|
-
db.execute_sql(
|
|
198
|
-
"UPDATE items SET #{fields} WHERE id = ?",
|
|
199
|
-
get_item_values(item)
|
|
200
|
-
)
|
|
161
|
+
def get_insert_values(item)
|
|
162
|
+
@database_fields ||= ITEM_FIELDS
|
|
163
|
+
values = @database_fields.map { |field| item[field] }
|
|
164
|
+
[item['id'], *values]
|
|
201
165
|
end
|
|
202
166
|
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
# @param item [Hash] Hash containing item data
|
|
206
|
-
# @param include_id [Boolean] Whether to include ID at start (insert) or end (update)
|
|
207
|
-
# @return [Array] Array of values for database operation
|
|
208
|
-
def get_item_values(item, include_id_at_start: false)
|
|
167
|
+
def get_update_values(item)
|
|
209
168
|
@database_fields ||= ITEM_FIELDS
|
|
210
|
-
|
|
211
|
-
|
|
169
|
+
@database_fields.map { |field| item[field] }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def get_item_values(item, include_id_at_start: false)
|
|
173
|
+
include_id_at_start ? get_insert_values(item) : get_update_values(item)
|
|
212
174
|
end
|
|
213
175
|
end
|
|
214
176
|
end
|