nextjs-rewrite #5

Merged
david merged 56 commits from nextjs-rewrite into main 2026-03-30 02:30:16 +00:00
31 changed files with 865 additions and 2 deletions
Showing only changes of commit 1268889a78 - Show all commits
+2 -1
View File
@@ -17,8 +17,9 @@ gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6" gem "rom-sql", "~> 3.6"
gem "sqlite3" gem "sqlite3"
gem "good_job", "~> 4.13"
group :development do group :development do
gem "hanami-webconsole", "~> 2.1"
gem "guard-puma" gem "guard-puma"
end end
+31
View File
@@ -0,0 +1,31 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Create < EuchreCamp::Action
include Deps[repo: 'repositories.matches']
def handle(request, response)
match_params = request.params.to_h
match_params[:played_at] = Time.parse(match_params[:played_at]) if match_params[:played_at]
# Create the match
match = repo.create(match_params)
# Enqueue background job to calculate Elo
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
response.redirect_to("/admin/matches")
rescue StandardError => e
# Handle validation errors or other issues
puts "Error: #{e.message}"
puts e.backtrace.join("\n")
response.redirect_to("/admin/matches/new")
end
end
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Index < EuchreCamp::Action
include Deps[repo: 'repositories.matches']
def handle(_request, response)
matches = repo.all
response.render(view, matches: matches)
end
end
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class New < EuchreCamp::Action
include Deps[repo: 'repositories.players']
def handle(_request, response)
players = repo.all
response.render(view, players: players)
end
end
end
end
end
end
@@ -0,0 +1,86 @@
# frozen_string_literal: true
require "csv"
module EuchreCamp
module Actions
module Admin
module Matches
class ProcessUpload < EuchreCamp::Action
include Deps[repo: 'repositories.matches']
def handle(request, response)
# In Hanami, file uploads are in request.params
file = request.params[:csv_file]
unless file && file[:tempfile]
response.redirect_to("/admin/matches/upload")
return
end
matches_created = 0
errors = []
# Parse CSV
CSV.foreach(file[:tempfile].path, headers: true, header_converters: :symbol) do |row|
begin
# Map CSV columns to our database structure
# Expected columns: played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
# Find or create players by name
player_1 = find_or_create_player(row[:team_1_p1_name])
player_2 = find_or_create_player(row[:team_1_p2_name])
player_3 = find_or_create_player(row[:team_2_p1_name])
player_4 = find_or_create_player(row[:team_2_p2_name])
played_at = begin
Time.parse(row[:played_at])
rescue
Time.now
end
match_data = {
played_at: played_at,
team_1_p1_id: player_1[:id],
team_1_p2_id: player_2[:id],
team_1_score: row[:team_1_score].to_i,
team_2_p1_id: player_3[:id],
team_2_p2_id: player_4[:id],
team_2_score: row[:team_2_score].to_i,
status: 'completed'
}
match = repo.create(match_data)
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
matches_created += 1
rescue => e
errors << "Row #{row}: #{e.message}"
end
end
response.flash[:message] = "Created #{matches_created} matches.#{' Errors: ' + errors.join(', ') if errors.any?}"
response.redirect_to("/admin/matches")
end
private
def find_or_create_player(name)
rom = EuchreCamp::App["persistence.rom"]
return nil unless name
name = name.strip
player = rom.relations[:players].where(name: name).one
if player
player
else
rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
rom.relations[:players].where(name: name).one
end
end
end
end
end
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
require "csv"
module EuchreCamp
module Actions
module Admin
module Matches
class Upload < EuchreCamp::Action
def handle(request, response)
response.render(view)
end
end
end
end
end
end
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Players
class Rankings < EuchreCamp::Action
include Deps[repo: 'repositories.players']
def handle(_request, response)
# Get all players and sort by current Elo
players = repo.all.sort_by { |p| -p.current_elo }
response.render(view, players: players)
end
end
end
end
end
+86
View File
@@ -0,0 +1,86 @@
# frozen_string_literal: true
module EuchreCamp
module Jobs
class CalculateEloJob
def self.perform_later(match_id)
# Enqueue job using GoodJob's database directly
rom = EuchreCamp::App["persistence.rom"]
job_params = {
queue_name: "default",
priority: 0,
serialized_params: { "match_id" => match_id }.to_json,
scheduled_at: Time.now,
created_at: Time.now,
updated_at: Time.now,
job_class: "EuchreCamp::Jobs::CalculateEloJob",
executions_count: 0
}
rom.relations[:good_jobs].insert(job_params)
end
def self.perform(match_id)
# This is the actual job execution
match = find_match(match_id)
return unless match
# Get current player ratings
player_ratings = fetch_player_ratings(match)
# Calculate new ratings
calculation_results = EuchreCamp::Services::EloCalculator.calculate(
player_ratings: player_ratings,
team_1_players: [match[:team_1_p1_id], match[:team_1_p2_id]],
team_2_players: [match[:team_2_p1_id], match[:team_2_p2_id]],
team_1_score: match[:team_1_score],
team_2_score: match[:team_2_score]
)
# Update player ratings and create snapshots
update_player_ratings(calculation_results, match_id)
end
private
def self.find_match(match_id)
rom = EuchreCamp::App["persistence.rom"]
rom.relations[:matches].by_pk(match_id).one
end
def self.fetch_player_ratings(match)
rom = EuchreCamp::App["persistence.rom"]
player_ids = [
match[:team_1_p1_id],
match[:team_1_p2_id],
match[:team_2_p1_id],
match[:team_2_p2_id]
]
players = rom.relations[:players].where(id: player_ids).to_a
players.each_with_object({}) do |player, hash|
hash[player[:id]] = player[:current_elo] || 1000
end
end
def self.update_player_ratings(results, match_id)
rom = EuchreCamp::App["persistence.rom"]
results.each do |player_id, data|
# Update player's current Elo
rom.relations[:players].where(id: player_id).update(current_elo: data[:rating_after])
# Create Elo snapshot record
rom.relations[:elo_snapshots].insert(
player_id: player_id,
match_id: match_id,
rating_before: data[:rating_before],
rating_after: data[:rating_after],
rating_change: data[:rating_change]
)
end
end
end
end
end
+19
View File
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class Matches < ::EuchreCamp::Repository[:matches]
commands :create, update: :by_pk, delete: :by_pk
def all
matches.to_a
end
def by_id(id)
matches.by_pk(id).one!
end
end
end
end
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
module EuchreCamp
module Services
class EloCalculator
# Standard Elo K-factor
K_FACTOR = 32
# Calculate Elo ratings for a 2v2 Euchre match
#
# @param player_ratings [Hash] player_id => current_rating
# @param team_1_players [Array<Integer>] player IDs for team 1
# @param team_2_players [Array<Integer>] player IDs for team 2
# @param team_1_score [Integer] score for team 1
# @param team_2_score [Integer] score for team 2
# @return [Hash] player_id => { rating_before:, rating_after:, rating_change: }
def self.calculate(player_ratings:, team_1_players:, team_2_players:, team_1_score:, team_2_score:)
# Validate inputs
raise ArgumentError, "Team 1 must have exactly 2 players" unless team_1_players.length == 2
raise ArgumentError, "Team 2 must have exactly 2 players" unless team_2_players.length == 2
# Determine winner (first to 10 points)
team_1_wins = team_1_score >= 10
team_2_wins = team_2_score >= 10
unless team_1_wins || team_2_wins
raise ArgumentError, "One team must reach 10 points to win"
end
if team_1_wins && team_2_wins
raise ArgumentError, "Only one team can win (both reached 10 points)"
end
winner = team_1_wins ? :team_1 : :team_2
# Calculate average ratings for each team
team_1_avg = team_1_players.map { |id| player_ratings[id] }.sum / 2.0
team_2_avg = team_2_players.map { |id| player_ratings[id] }.sum / 2.0
# Calculate expected scores using Elo formula
# E = 1 / (1 + 10^((R_opp - R_team) / 400))
expected_team_1 = 1.0 / (1.0 + 10.0**((team_2_avg - team_1_avg) / 400.0))
expected_team_2 = 1.0 / (1.0 + 10.0**((team_1_avg - team_2_avg) / 400.0))
# Actual scores (1 for win, 0 for loss)
actual_team_1 = team_1_wins ? 1.0 : 0.0
actual_team_2 = team_2_wins ? 1.0 : 0.0
# Calculate rating changes for each player
results = {}
# Team 1 players
team_1_players.each do |player_id|
old_rating = player_ratings[player_id]
rating_change = K_FACTOR * (actual_team_1 - expected_team_1)
new_rating = (old_rating + rating_change).round
results[player_id] = {
rating_before: old_rating,
rating_after: new_rating,
rating_change: rating_change.round
}
end
# Team 2 players
team_2_players.each do |player_id|
old_rating = player_ratings[player_id]
rating_change = K_FACTOR * (actual_team_2 - expected_team_2)
new_rating = (old_rating + rating_change).round
results[player_id] = {
rating_before: old_rating,
rating_after: new_rating,
rating_change: rating_change.round
}
end
results
end
end
end
end
@@ -0,0 +1,36 @@
<h1>Match Administration</h1>
<p><a href="/admin/matches/new">Enter New Match</a> | <a href="/admin/matches/upload">Upload CSV</a></p>
<% if matches.any? %>
<table>
<thead>
<tr>
<th>ID</th>
<th>Date</th>
<th>Team 1</th>
<th>Score</th>
<th>Team 2</th>
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<% matches.each do |match| %>
<tr>
<td><%= match.id %></td>
<td><%= match.played_at&.strftime('%Y-%m-%d %H:%M') %></td>
<td><%= match.team_1_p1_id %>/<%= match.team_1_p2_id %></td>
<td><%= match.team_1_score %></td>
<td><%= match.team_2_p1_id %>/<%= match.team_2_p2_id %></td>
<td><%= match.team_2_score %></td>
<td><%= match.status %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No matches entered yet.</p>
<% end %>
<p><a href="/admin/players">Manage Players</a></p>
+66
View File
@@ -0,0 +1,66 @@
<h1>Enter New Match</h1>
<form action="/admin/matches" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label for="played_at">Date/Time:</label>
<input type="datetime-local" id="played_at" name="played_at" required value="<%= Time.now.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<fieldset>
<legend>Team 1</legend>
<div>
<label for="team_1_p1_id">Player 1:</label>
<select id="team_1_p1_id" name="team_1_p1_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_1_p2_id">Player 2:</label>
<select id="team_1_p2_id" name="team_1_p2_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_1_score">Score:</label>
<input type="number" id="team_1_score" name="team_1_score" min="0" max="10" required value="10">
</div>
</fieldset>
<fieldset>
<legend>Team 2</legend>
<div>
<label for="team_2_p1_id">Player 1:</label>
<select id="team_2_p1_id" name="team_2_p1_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_2_p2_id">Player 2:</label>
<select id="team_2_p2_id" name="team_2_p2_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_2_score">Score:</label>
<input type="number" id="team_2_score" name="team_2_score" min="0" max="10" required value="7">
</div>
</fieldset>
<button type="submit">Save Match</button>
</form>
<p><a href="/admin/matches">Back to Matches</a></p>
@@ -0,0 +1,26 @@
<h1>Upload Matches (CSV)</h1>
<p>Upload a CSV file with the following columns:</p>
<pre>
played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
</pre>
<form action="/admin/matches/process_upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label for="csv_file">CSV File:</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<button type="submit">Upload</button>
</form>
<p><a href="/admin/matches">Back to Matches</a></p>
<h2>Example CSV:</h2>
<pre>
played_at,team_1_p1_name,team_1_p2_name,team_1_score,team_2_p1_name,team_2_p2_name,team_2_score
2026-03-26 10:00,Alice,Bob,10,Charlie,David,7
2026-03-26 10:30,Alice,Bob,10,Charlie,David,7
</pre>
+28
View File
@@ -0,0 +1,28 @@
<h1>Player Rankings</h1>
<% if players.any? %>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Elo Rating</th>
<th>Original Rating</th>
</tr>
</thead>
<tbody>
<% players.each_with_index do |player, index| %>
<tr>
<td><%= index + 1 %></td>
<td><%= player.name %></td>
<td><strong><%= player.current_elo %></strong></td>
<td><%= player.rating %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No players found.</p>
<% end %>
<p><a href="/admin/players">Admin</a></p>
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Index < EuchreCamp::View
expose :matches
end
end
end
end
end
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class New < EuchreCamp::View
expose :players
end
end
end
end
end
+12
View File
@@ -0,0 +1,12 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Upload < EuchreCamp::View
end
end
end
end
end
+11
View File
@@ -0,0 +1,11 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Players
class Rankings < EuchreCamp::View
expose :players
end
end
end
end
+57
View File
@@ -0,0 +1,57 @@
# frozen_string_literal: true
require_relative "../config/app"
# Boot the Hanami app to ensure persistence is available
EuchreCamp::App.boot
# Simple worker that polls for jobs and executes them
def poll_and_execute_jobs
rom = EuchreCamp::App["persistence.rom"]
puts "Worker started, waiting for jobs..."
loop do
# Find a pending job
job = rom.relations[:good_jobs].where(finished_at: nil).order(:scheduled_at).limit(1).one
if job
begin
# Mark as performed
rom.relations[:good_jobs].where(id: job[:id]).update(
performed_at: Time.now,
executions_count: (job[:executions_count] || 0) + 1
)
# Execute the job
params = JSON.parse(job[:serialized_params])
job_class = Object.const_get(job[:job_class])
if params["match_id"]
job_class.perform(params["match_id"])
else
# Legacy format for backwards compatibility
job_class.perform(*params["arguments"])
end
# Mark as finished
rom.relations[:good_jobs].where(id: job[:id]).update(finished_at: Time.now)
puts "Executed job #{job[:id]}: #{job[:job_class]}"
rescue => e
# Mark as failed
rom.relations[:good_jobs].where(id: job[:id]).update(
finished_at: Time.now,
error: e.message
)
puts "Failed job #{job[:id]}: #{e.message}"
puts e.backtrace.first(5).join("\n")
end
else
# No jobs, wait a bit
sleep 1
end
end
end
poll_and_execute_jobs
+5
View File
@@ -1,8 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require "hanami" require "hanami"
require "good_job"
module EuchreCamp module EuchreCamp
class App < Hanami::App class App < Hanami::App
config.root = File.expand_path("..", __dir__)
# Enable sessions
config.actions.sessions = :cookie, {secret: ENV.fetch("SESSION_SECRET", "change_me_in_production")}
end end
end end
+6
View File
@@ -0,0 +1,6 @@
# frozen_string_literal: true
require "active_job"
# Configure Active Job to use GoodJob
ActiveJob::Base.queue_adapter = :good_job
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
require "good_job"
# GoodJob configuration for Hanami
# Since GoodJob is primarily designed for Rails, we configure it directly
GoodJob.preserve_job_records = true
GoodJob.retry_on_unhandled_error = false
GoodJob.on_thread_error = ->(exception) { puts "GoodJob error: #{exception.message}" }
# Set execution mode based on environment
if ENV["GOOD_JOB_EXECUTION_MODE"]
GoodJob.execution_mode = ENV["GOOD_JOB_EXECUTION_MODE"].to_sym
elsif ENV["RACK_ENV"] == "test"
GoodJob.execution_mode = :inline
elsif ENV["RACK_ENV"] == "development"
GoodJob.execution_mode = :async
else
GoodJob.execution_mode = :external
end
+10
View File
@@ -4,6 +4,7 @@ module EuchreCamp
class Routes < Hanami::Routes class Routes < Hanami::Routes
# Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details. # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details.
get "/players/:id", to: "players.show" get "/players/:id", to: "players.show"
get "/rankings", to: "players.rankings"
get "/admin/players/new", to: "admin.players.new" get "/admin/players/new", to: "admin.players.new"
post "/admin/players", to: "admin.players.create" post "/admin/players", to: "admin.players.create"
@@ -11,5 +12,14 @@ module EuchreCamp
get "/admin/players/:id/edit", to: "admin.players.edit" get "/admin/players/:id/edit", to: "admin.players.edit"
patch "/admin/players/:id", to: "admin.players.update" patch "/admin/players/:id", to: "admin.players.update"
delete "/admin/players/:id", to: "admin.players.destroy" delete "/admin/players/:id", to: "admin.players.destroy"
# Match routes
get "/admin/matches/new", to: "admin.matches.new"
post "/admin/matches", to: "admin.matches.create"
get "/admin/matches", to: "admin.matches.index"
# CSV Upload routes
get "/admin/matches/upload", to: "admin.matches.upload"
post "/admin/matches/process_upload", to: "admin.matches.process_upload"
end end
end end
+5
View File
@@ -0,0 +1,5 @@
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
#HttpOnly_localhost FALSE / FALSE 0 rack.session ASyilKVTxn1iBCRZVUIw0JytfvCxi0S7cf1Cyrqsal0zKn_qN-XMmRdfPBJSFliFhkK0_LVlDU99FIVtgnT4FnLijl8lYpcQzF1dVtPOGvEQpYLM5-44U9nsm1angx6MrLhNuwwizUIUHTtJFpvM7iA5hU1S5PwHF5hXvTYy1zQAVMJOsiF1rUORTb-pbsImZButiYE1qw29AF6RxVn8LnaM3mGBfA7b8jBaSe6gnaEjeJ5gbQMrI8o0efXcmJW2ViovTvto-gIvElOL0H3cDV-AcvmBp_QUpL3buZUD-A65c2lT91s92R19pkS1AM_hHTN9v1gGlcY2eyCVoqC4RYI-RG_HYdp7fkWEnf0rb8uXDNh9GJdqg0TPMUu0P_klRs7Jdx0bFJcAjWMzpQhYRmisGJ8Mf7RN6XmtYDLyqopcMBS3U5mpDpk6p1Rf_yr6vQ%3D%3D
@@ -0,0 +1,53 @@
# frozen_string_literal: true
ROM::SQL.migration do
change do
create_table :good_jobs do
primary_key :id
column :queue_name, String
column :priority, Integer, default: 0
column :serialized_params, String
column :scheduled_at, DateTime
column :performed_at, DateTime
column :finished_at, DateTime
column :error, String
column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
column :active_job_id, String
column :concurrency_key, String
column :sequence, Integer
column :executions_count, Integer
column :job_class, String
column :error_event, Integer
column :labels, String
column :locked_by_id, String
column :locked_at, DateTime
end
add_index :good_jobs, [:scheduled_at, :priority, :id], name: "index_good_jobs_on_scheduled_at"
create_table :good_job_processes do
primary_key :id
column :state, String
column :started_at, DateTime
column :singleton_key, String
column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
end
create_table :good_job_execution_errors do
primary_key :id
column :active_job_id, String
column :error, String
column :created_at, DateTime, null: false
end
create_table :good_job_settings do
primary_key :id
column :key, String
column :value, String
column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
end
end
end
@@ -0,0 +1,42 @@
# frozen_string_literal: true
ROM::SQL.migration do
change do
# Drop and recreate matches table with new structure
drop_table?(:matches)
create_table :matches do
primary_key :id
foreign_key :event_id, :events
column :played_at, DateTime
column :team_1_p1_id, Integer
column :team_1_p2_id, Integer
column :team_2_p1_id, Integer
column :team_2_p2_id, Integer
column :team_1_score, Integer
column :team_2_score, Integer
column :status, String, default: 'completed'
end
# Add current_elo to players table
alter_table :players do
add_column :current_elo, Integer, default: 1000
end
# Create elo_snapshots table
create_table :elo_snapshots do
primary_key :id
foreign_key :player_id, :players, null: false
foreign_key :match_id, :matches, null: false
column :rating_before, Integer, null: false
column :rating_after, Integer, null: false
column :rating_change, Integer, null: false
column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP
end
# Add indexes for performance
add_index :elo_snapshots, [:player_id, :match_id], unique: true
add_index :matches, :played_at
add_index :matches, :status
end
end
BIN
View File
Binary file not shown.
@@ -0,0 +1,19 @@
# frozen_string_literal: true
module EuchreCamp
module Persistence
module Relations
class EloSnapshots < ROM::Relation[:sql]
schema(:elo_snapshots, infer: true) do
attribute :id, ROM::Types::Integer, primary_key: true
attribute :player_id, ROM::Types::Integer
attribute :match_id, ROM::Types::Integer
attribute :rating_before, ROM::Types::Integer
attribute :rating_after, ROM::Types::Integer
attribute :rating_change, ROM::Types::Integer
attribute :created_at, ROM::Types::DateTime
end
end
end
end
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
module EuchreCamp
module Persistence
module Relations
class GoodJobs < ROM::Relation[:sql]
schema(:good_jobs, infer: true) do
attribute :id, ROM::Types::Integer
attribute :queue_name, ROM::Types::String
attribute :priority, ROM::Types::Integer
attribute :serialized_params, ROM::Types::String
attribute :scheduled_at, ROM::Types::DateTime.optional
attribute :performed_at, ROM::Types::DateTime.optional
attribute :finished_at, ROM::Types::DateTime.optional
attribute :error, ROM::Types::String.optional
attribute :created_at, ROM::Types::DateTime
attribute :updated_at, ROM::Types::DateTime
attribute :active_job_id, ROM::Types::String.optional
attribute :concurrency_key, ROM::Types::String.optional
attribute :sequence, ROM::Types::Integer.optional
attribute :executions_count, ROM::Types::Integer.optional
attribute :job_class, ROM::Types::String.optional
attribute :error_event, ROM::Types::Integer.optional
attribute :labels, ROM::Types::String.optional
attribute :locked_by_id, ROM::Types::String.optional
attribute :locked_at, ROM::Types::DateTime.optional
end
end
end
end
end
@@ -0,0 +1,29 @@
# frozen_string_literal: true
module EuchreCamp
module Persistence
module Relations
class Matches < ROM::Relation[:sql]
schema(:matches, infer: true) do
attribute :id, ROM::Types::Integer, primary_key: true
attribute :event_id, ROM::Types::Integer.optional
attribute :played_at, ROM::Types::DateTime.optional
attribute :team_1_p1_id, ROM::Types::Integer
attribute :team_1_p2_id, ROM::Types::Integer
attribute :team_2_p1_id, ROM::Types::Integer
attribute :team_2_p2_id, ROM::Types::Integer
attribute :team_1_score, ROM::Types::Integer
attribute :team_2_score, ROM::Types::Integer
attribute :status, ROM::Types::String.default('completed')
associations do
belongs_to :team_1_player, relation: :players, foreign_key: :team_1_p1_id
belongs_to :team_2_player, relation: :players, foreign_key: :team_1_p2_id
belongs_to :team_3_player, relation: :players, foreign_key: :team_2_p1_id
belongs_to :team_4_player, relation: :players, foreign_key: :team_2_p2_id
end
end
end
end
end
end
@@ -1,8 +1,15 @@
# frozen_string_literal: true
module EuchreCamp module EuchreCamp
module Persistence module Persistence
module Relations module Relations
class Players < ROM::Relation[:sql] class Players < ROM::Relation[:sql]
schema(:players, infer: true) schema(:players, infer: true) do
attribute :id, ROM::Types::Integer, primary_key: true
attribute :name, ROM::Types::String
attribute :rating, ROM::Types::Integer
attribute :current_elo, ROM::Types::Integer.default(1000)
end
end end
end end
end end