feat: add database schema v1
This commit is contained in:
6
.env
6
.env
@@ -1,3 +1,3 @@
|
|||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=cardgame
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=cardgame
|
||||||
POSTGRES_DB=card_game_db
|
POSTGRES_DB=cardgame
|
||||||
|
|||||||
@@ -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
114
backend/app/models.py
Normal 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)
|
||||||
Reference in New Issue
Block a user