Extract Sequel models from raw SQL in web.rb
Routes used hand-built INSERT/SELECT with manual timestamp
management. Consolidating into model classes with the timestamps
plugin fixes the NOT NULL constraint failures on users.updated_at.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/Gemfile b/Gemfile
index 734a2bd..e263aa0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -16,5 +16,6 @@ group :development do
gem "rack-test"
gem "rake"
gem "rbs-inline", require: false
+ gem "ruby-lsp"
gem "steep"
end
diff --git a/Gemfile.lock b/Gemfile.lock
index a13c2d2..13e292f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -77,6 +77,10 @@ GEM
zeitwerk
roda (3.100.0)
rack
+ ruby-lsp (0.26.6)
+ language_server-protocol (~> 3.17.0)
+ prism (>= 1.2, < 2.0)
+ rbs (>= 3, < 5)
securerandom (0.4.1)
sequel (5.101.0)
bigdecimal
@@ -140,6 +144,7 @@ DEPENDENCIES
rake
rbs-inline
roda
+ ruby-lsp
sequel
sqlite3
steep
@@ -189,6 +194,7 @@ CHECKSUMS
rbs-inline (0.12.0) sha256=0b84ec69fa5b3deb89e60f08d9b47966c9e26a222c7c65b5014857a3b10c62b2
refract (1.1.0) sha256=ee3b9627e39f7692831101e2fedd73e0d09a592ff5d5c05f171d14211fc7a9c7
roda (3.100.0) sha256=35d36f43c68d2bd1974dc77ade8c873558265a191018e1ada88ca662f1fa8e62
+ ruby-lsp (0.26.6) sha256=1f50b21cb300586d7742190fc8446c18387f488aa0e9d276cf2bc7f3684ccda2
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
sequel (5.101.0) sha256=d2ae3fd997a7c4572e8357918e777869faf90dc19310fcd6332747122aed2b29
sqlite3 (2.9.0-aarch64-linux-gnu) sha256=cfe1e0216f46d7483839719bf827129151e6c680317b99d7b8fc1597a3e13473
diff --git a/lib/models.rb b/lib/models.rb
new file mode 100644
index 0000000..6cf1a5a
--- /dev/null
+++ b/lib/models.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require_relative "db"
+
+Sequel::Model.plugin :timestamps, update_on_create: true
+
+class User < Sequel::Model
+ one_to_many :series
+
+ def self.upsert(login:, name:)
+ user = find_or_create(login: login) { |u| u.name = name }
+ user.update(name: name) if user.name != name
+ user
+ end
+end
+
+class Series < Sequel::Model
+ many_to_one :user
+ one_to_many :tasks
+
+ INTERVAL_UNITS = %w[day week month quarter year].freeze
+
+ def active_task
+ tasks_dataset.where(completed_at: nil).first
+ end
+
+ def self.create_with_first_task(user:, note:, interval_unit:, interval_count:, first_due_date:)
+ DB.transaction do
+ series = create(
+ user_id: user.id,
+ note: note,
+ interval_unit: interval_unit,
+ interval_count: interval_count
+ )
+
+ Task.create(
+ series_id: series.id,
+ due_date: first_due_date
+ )
+
+ series
+ end
+ end
+end
+
+class Task < Sequel::Model
+ many_to_one :series
+
+ dataset_module do
+ def active
+ where(completed_at: nil)
+ end
+
+ def for_user(user)
+ join(:series, id: :series_id)
+ .where(Sequel[:series][:user_id] => user.id)
+ .select_all(:tasks)
+ .select_append(
+ Sequel[:series][:note],
+ Sequel[:series][:interval_unit],
+ Sequel[:series][:interval_count]
+ )
+ end
+
+ def by_due_date
+ order(Sequel[:tasks][:due_date])
+ end
+ end
+end
diff --git a/lib/web.rb b/lib/web.rb
index b66ca49..28fe64d 100644
--- a/lib/web.rb
+++ b/lib/web.rb
@@ -2,7 +2,7 @@
require "roda"
-require_relative "db"
+require_relative "models"
require_relative "views/home"
class Web < Roda
@@ -14,27 +14,14 @@ class Web < Roda
return unless login
name = env["HTTP_TAILSCALE_USER_NAME"]
- now = Time.now
-
- DB[:users]
- .insert_conflict(target: :login, update: { name: name, updated_at: now })
- .insert(login: login, name: name, created_at: now, updated_at: now)
-
- DB[:users].first(login: login)
+ User.upsert(login: login, name: name)
end
- INTERVAL_UNITS = %w[day week month quarter year].freeze
-
route do |r|
r.halt 403 unless current_user
r.root do
- tasks = DB[:tasks]
- .join(:series, id: :series_id)
- .where(completed_at: nil, user_id: current_user[:id])
- .order(Sequel[:tasks][:due_date])
- .all
-
+ tasks = Task.active.for_user(current_user).by_due_date.all
Views::Home.new(current_user:, tasks:).call
end
@@ -46,7 +33,7 @@ class Web < Roda
first_due_date = r.params["first_due_date"].to_s
r.halt 422 if note.empty?
- r.halt 422 unless INTERVAL_UNITS.include?(interval_unit)
+ r.halt 422 unless Series::INTERVAL_UNITS.include?(interval_unit)
r.halt 422 unless interval_count >= 1
begin
@@ -55,24 +42,13 @@ class Web < Roda
r.halt 422
end
- now = Time.now
- DB.transaction do
- series_id = DB[:series].insert(
- user_id: current_user[:id],
- note: note,
- interval_unit: interval_unit,
- interval_count: interval_count,
- created_at: now,
- updated_at: now
- )
-
- DB[:tasks].insert(
- series_id: series_id,
- due_date: due_date,
- created_at: now,
- updated_at: now
- )
- end
+ Series.create_with_first_task(
+ user: current_user,
+ note: note,
+ interval_unit: interval_unit,
+ interval_count: interval_count,
+ first_due_date: due_date
+ )
r.redirect "/"
end