feat: add database schema v1

This commit is contained in:
2026-02-09 14:25:58 +01:00
parent 8b200a546c
commit 8b883258ba
3 changed files with 122 additions and 4 deletions

6
.env
View File

@@ -1,3 +1,3 @@
POSTGRES_USER=postgres POSTGRES_USER=cardgame
POSTGRES_PASSWORD=postgres POSTGRES_PASSWORD=cardgame
POSTGRES_DB=card_game_db POSTGRES_DB=cardgame

View File

@@ -1,7 +1,11 @@
from fastapi import FastAPI, Depends, HTTPException from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import text from sqlalchemy import text
from app.database import get_db from app.database import get_db, engine
from app import models
# Create all tables
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="Card Game Backend") app = FastAPI(title="Card Game Backend")

114
backend/app/models.py Normal file
View File

@@ -0,0 +1,114 @@
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Enum, UniqueConstraint
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
import uuid
def generate_uuid():
return str(uuid.uuid4())
class User(Base):
__tablename__ = "users"
id = Column(String, primary_key=True, default=generate_uuid)
email = Column(String, unique=True, nullable=False)
password_hash = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
profile = relationship("UserProfile", back_populates="user", uselist=False)
wallet = relationship("Wallet", back_populates="user", uselist=False)
cards = relationship("UserCard", back_populates="user")
transactions = relationship("WalletTransaction", back_populates="user")
chest_opens = relationship("ChestOpen", back_populates="user")
class UserProfile(Base):
__tablename__ = "user_profiles"
user_id = Column(String, ForeignKey("users.id"), primary_key=True)
nickname = Column(String, unique=True, nullable=False)
is_collection_public = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="profile")
class Wallet(Base):
__tablename__ = "wallets"
user_id = Column(String, ForeignKey("users.id"), primary_key=True)
balance = Column(Integer, nullable=False, default=0)
user = relationship("User", back_populates="wallet")
class WalletTransaction(Base):
__tablename__ = "wallet_transactions"
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
type = Column(String, nullable=False) # CREDIT / DEBIT
amount = Column(Integer, nullable=False)
reason = Column(String, nullable=False) # REGISTER_BONUS, CHEST_OPEN, etc.
reference_id = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="transactions")
class Card(Base):
__tablename__ = "cards"
id = Column(String, primary_key=True, default=generate_uuid)
name = Column(String, nullable=False)
rarity = Column(String, nullable=False) # COMMON, RARE, LEGENDARY
created_at = Column(DateTime(timezone=True), server_default=func.now())
owners = relationship("UserCard", back_populates="card")
class Chest(Base):
__tablename__ = "chests"
id = Column(String, primary_key=True, default=generate_uuid)
name = Column(String, nullable=False)
cost_gold = Column(Integer, nullable=False)
cards_per_open = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
class UserCard(Base):
__tablename__ = "user_cards"
user_id = Column(String, ForeignKey("users.id"), primary_key=True)
card_id = Column(String, ForeignKey("cards.id"), primary_key=True)
obtained_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="cards")
card = relationship("Card", back_populates="owners")
class ChestOpen(Base):
__tablename__ = "chest_opens"
id = Column(String, primary_key=True, default=generate_uuid)
user_id = Column(String, ForeignKey("users.id"), nullable=False)
chest_id = Column(String, ForeignKey("chests.id"), nullable=False)
spent_gold = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
user = relationship("User", back_populates="chest_opens")
items = relationship("ChestOpenItem", back_populates="chest_open")
class ChestOpenItem(Base):
__tablename__ = "chest_open_items"
id = Column(String, primary_key=True, default=generate_uuid)
chest_open_id = Column(String, ForeignKey("chest_opens.id"), nullable=False)
card_id = Column(String, ForeignKey("cards.id"), nullable=True) # Nullable if pure conversion without card entity reference logic? Actually SOP says card NON possessed -> NEW. If possessed -> CONVERTED. Usually still references the card.
rarity = Column(String, nullable=False)
outcome_type = Column(String, nullable=False) # NEW / CONVERTED
converted_gold = Column(Integer, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
chest_open = relationship("ChestOpen", back_populates="items")
class IdempotencyKey(Base):
__tablename__ = "idempotency_keys"
key = Column(String, nullable=False)
user_id = Column(String, nullable=False)
endpoint = Column(String, nullable=False)
response_body = Column(String, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
__table_args__ = (
UniqueConstraint('key', 'user_id', 'endpoint', name='uix_idempotency_key_user_endpoint'),
{"extend_existing": True} # Just in case
)
# Note: Using composite primary key might be better but SOP says "UNIQUE (key, user_id, endpoint)".
# Let's add an ID or make the composite PK. SQLAlchemy needs a PK.
id = Column(Integer, primary_key=True, autoincrement=True)