Adds types

This commit is contained in:
2026-03-29 02:37:49 -04:00
parent 63276ef8ca
commit 323484a33a
44 changed files with 1273 additions and 121 deletions

View File

@@ -1,4 +1,8 @@
# typed: true
class ApiToken < ApplicationRecord
extend T::Sig
belongs_to :user
# Validations
@@ -11,16 +15,19 @@ class ApiToken < ApplicationRecord
scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
# Instance methods
sig { returns(T::Boolean) }
def expired?
expires_at.present? && expires_at < Time.current
end
sig { void }
def touch_last_used!
update_column(:last_used_at, Time.current)
end
private
sig { void }
def generate_token
self.token ||= SecureRandom.urlsafe_base64(32)
end

View File

@@ -1,4 +1,7 @@
# typed: true
class Collection < ApplicationRecord
extend T::Sig
# Associations
belongs_to :user
belongs_to :parent_collection, class_name: "Collection", optional: true
@@ -15,30 +18,36 @@ class Collection < ApplicationRecord
scope :root_collections, -> { where(parent_collection_id: nil) }
# Instance methods
sig { returns(Integer) }
def game_count
games.count
end
sig { returns(Integer) }
def total_game_count
game_count + subcollections.sum(&:total_game_count)
end
sig { returns(T::Boolean) }
def root?
parent_collection_id.nil?
end
sig { returns(T::Boolean) }
def subcollection?
parent_collection_id.present?
end
private
sig { void }
def cannot_be_own_parent
if parent_collection_id.present? && parent_collection_id == id
errors.add(:parent_collection_id, "cannot be itself")
end
end
sig { void }
def subcollection_depth_limit
if parent_collection.present? && parent_collection.parent_collection.present?
errors.add(:parent_collection_id, "cannot nest more than one level deep")

View File

@@ -1,4 +1,7 @@
# typed: true
class CollectionGame < ApplicationRecord
extend T::Sig
belongs_to :collection
belongs_to :game

View File

@@ -1,4 +1,7 @@
# typed: true
class Game < ApplicationRecord
extend T::Sig
# Associations
belongs_to :user
belongs_to :platform
@@ -54,12 +57,14 @@ class Game < ApplicationRecord
scope :needs_igdb_review, -> { joins(:igdb_match_suggestions).where(igdb_match_suggestions: { status: "pending" }).distinct }
# Class methods
sig { params(query: String).returns(T.untyped) }
def self.search(query)
where("title ILIKE ?", "%#{sanitize_sql_like(query)}%")
end
private
sig { void }
def set_date_added
self.date_added ||= Date.current
end

View File

@@ -1,4 +1,7 @@
# typed: true
class GameGenre < ApplicationRecord
extend T::Sig
belongs_to :game
belongs_to :genre

View File

@@ -1,4 +1,7 @@
# typed: true
class Genre < ApplicationRecord
extend T::Sig
# Associations
has_many :game_genres, dependent: :destroy
has_many :games, through: :game_genres

View File

@@ -1,4 +1,7 @@
# typed: true
class IgdbGame < ApplicationRecord
extend T::Sig
# Associations
has_many :igdb_match_suggestions
has_many :games, foreign_key: :igdb_id, primary_key: :igdb_id
@@ -12,10 +15,12 @@ class IgdbGame < ApplicationRecord
scope :recent, -> { order(last_synced_at: :desc) }
# Instance methods
sig { void }
def increment_match_count!
increment!(:match_count)
end
sig { params(size: String).returns(T.nilable(String)) }
def cover_image_url(size = "cover_big")
return nil unless cover_url.present?
# IGDB uses image IDs like "co1234"

View File

@@ -24,7 +24,7 @@ class IgdbMatchSuggestion < ApplicationRecord
def approve!
transaction do
update!(status: "approved", reviewed_at: Time.current)
# Update the game with the matched IGDB ID
game.update!(
igdb_id: igdb_id,
@@ -49,10 +49,10 @@ class IgdbMatchSuggestion < ApplicationRecord
end
igdb_game_record.increment_match_count!
# Map and assign IGDB genres to the game
sync_genres_to_game if igdb_genres.present?
# Reject all other pending suggestions for this game
game.igdb_match_suggestions
.where.not(id: id)
@@ -131,13 +131,13 @@ class IgdbMatchSuggestion < ApplicationRecord
igdb_genres.each do |igdb_genre_name|
# Try exact match first
local_genre = Genre.find_by("LOWER(name) = ?", igdb_genre_name.downcase)
# Try mapped name
if local_genre.nil? && genre_mappings[igdb_genre_name]
mapped_name = genre_mappings[igdb_genre_name]
local_genre = Genre.find_by("LOWER(name) = ?", mapped_name.downcase)
end
# Add genre to game if found and not already assigned
if local_genre && !game.genres.include?(local_genre)
game.genres << local_genre

View File

@@ -1,4 +1,7 @@
# typed: true
class Platform < ApplicationRecord
extend T::Sig
# Associations
has_many :games, dependent: :restrict_with_error
has_many :items, dependent: :restrict_with_error

View File

@@ -1,4 +1,8 @@
# typed: true
class User < ApplicationRecord
extend T::Sig
has_secure_password
# Associations
@@ -10,7 +14,7 @@ class User < ApplicationRecord
# Validations
validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :username, presence: true, uniqueness: { case_sensitive: false },
validates :username, presence: true, uniqueness: { case_sensitive: false },
length: { minimum: 3, maximum: 30 },
format: { with: /\A[a-zA-Z0-9_]+\z/, message: "only allows letters, numbers, and underscores" }
validates :password, length: { minimum: 8 }, if: -> { password.present? }
@@ -20,22 +24,26 @@ class User < ApplicationRecord
before_save :downcase_email
# Instance methods
sig { returns(T::Boolean) }
def generate_password_reset_token
self.password_reset_token = SecureRandom.urlsafe_base64
self.password_reset_sent_at = Time.current
save!
end
sig { returns(T::Boolean) }
def password_reset_expired?
password_reset_sent_at.nil? || password_reset_sent_at < 2.hours.ago
end
sig { returns(String) }
def theme_class
"theme-#{theme}"
end
private
sig { void }
def downcase_email
self.email = email.downcase if email.present?
end