Sepehr 550f3516db feat: Add PostgreSQL database infrastructure
- Add SQLAlchemy models for User, Translation, ApiKey, UsageLog, PaymentHistory
- Add database connection management with PostgreSQL/SQLite support
- Add repository layer for CRUD operations
- Add Alembic migration setup with initial migration
- Update auth_service to automatically use database when DATABASE_URL is set
- Update docker-compose.yml with PostgreSQL service and Redis (non-optional)
- Add database migration script (scripts/migrate_to_db.py)
- Update .env.example with database configuration
2025-12-31 10:56:19 +01:00

127 lines
5.8 KiB
Python

"""Initial database schema
Revision ID: 001_initial
Revises:
Create Date: 2024-12-31
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '001_initial'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Create users table
op.create_table(
'users',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('email', sa.String(255), unique=True, nullable=False),
sa.Column('name', sa.String(255), nullable=False),
sa.Column('password_hash', sa.String(255), nullable=False),
sa.Column('email_verified', sa.Boolean(), default=False),
sa.Column('is_active', sa.Boolean(), default=True),
sa.Column('avatar_url', sa.String(500), nullable=True),
sa.Column('plan', sa.String(20), default='free'),
sa.Column('subscription_status', sa.String(20), default='active'),
sa.Column('stripe_customer_id', sa.String(255), nullable=True),
sa.Column('stripe_subscription_id', sa.String(255), nullable=True),
sa.Column('docs_translated_this_month', sa.Integer(), default=0),
sa.Column('pages_translated_this_month', sa.Integer(), default=0),
sa.Column('api_calls_this_month', sa.Integer(), default=0),
sa.Column('extra_credits', sa.Integer(), default=0),
sa.Column('usage_reset_date', sa.DateTime()),
sa.Column('created_at', sa.DateTime()),
sa.Column('updated_at', sa.DateTime()),
sa.Column('last_login_at', sa.DateTime(), nullable=True),
)
op.create_index('ix_users_email', 'users', ['email'])
op.create_index('ix_users_email_active', 'users', ['email', 'is_active'])
op.create_index('ix_users_stripe_customer', 'users', ['stripe_customer_id'])
# Create translations table
op.create_table(
'translations',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('user_id', sa.String(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('original_filename', sa.String(255), nullable=False),
sa.Column('file_type', sa.String(10), nullable=False),
sa.Column('file_size_bytes', sa.BigInteger(), default=0),
sa.Column('page_count', sa.Integer(), default=0),
sa.Column('source_language', sa.String(10), default='auto'),
sa.Column('target_language', sa.String(10), nullable=False),
sa.Column('provider', sa.String(50), nullable=False),
sa.Column('status', sa.String(20), default='pending'),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('processing_time_ms', sa.Integer(), nullable=True),
sa.Column('characters_translated', sa.Integer(), default=0),
sa.Column('estimated_cost_usd', sa.Float(), default=0.0),
sa.Column('created_at', sa.DateTime()),
sa.Column('completed_at', sa.DateTime(), nullable=True),
)
op.create_index('ix_translations_user_date', 'translations', ['user_id', 'created_at'])
op.create_index('ix_translations_status', 'translations', ['status'])
# Create api_keys table
op.create_table(
'api_keys',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('user_id', sa.String(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('name', sa.String(100), nullable=False),
sa.Column('key_hash', sa.String(255), nullable=False),
sa.Column('key_prefix', sa.String(10), nullable=False),
sa.Column('is_active', sa.Boolean(), default=True),
sa.Column('scopes', sa.JSON(), default=list),
sa.Column('last_used_at', sa.DateTime(), nullable=True),
sa.Column('usage_count', sa.Integer(), default=0),
sa.Column('created_at', sa.DateTime()),
sa.Column('expires_at', sa.DateTime(), nullable=True),
)
op.create_index('ix_api_keys_prefix', 'api_keys', ['key_prefix'])
op.create_index('ix_api_keys_hash', 'api_keys', ['key_hash'])
# Create usage_logs table
op.create_table(
'usage_logs',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('user_id', sa.String(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('date', sa.DateTime(), nullable=False),
sa.Column('documents_count', sa.Integer(), default=0),
sa.Column('pages_count', sa.Integer(), default=0),
sa.Column('characters_count', sa.BigInteger(), default=0),
sa.Column('api_calls_count', sa.Integer(), default=0),
sa.Column('provider_breakdown', sa.JSON(), default=dict),
)
op.create_index('ix_usage_logs_user_date', 'usage_logs', ['user_id', 'date'], unique=True)
# Create payment_history table
op.create_table(
'payment_history',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('user_id', sa.String(36), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False),
sa.Column('stripe_payment_intent_id', sa.String(255), nullable=True),
sa.Column('stripe_invoice_id', sa.String(255), nullable=True),
sa.Column('amount_cents', sa.Integer(), nullable=False),
sa.Column('currency', sa.String(3), default='usd'),
sa.Column('payment_type', sa.String(50), nullable=False),
sa.Column('status', sa.String(20), nullable=False),
sa.Column('description', sa.String(255), nullable=True),
sa.Column('created_at', sa.DateTime()),
)
op.create_index('ix_payment_history_user', 'payment_history', ['user_id', 'created_at'])
def downgrade() -> None:
op.drop_table('payment_history')
op.drop_table('usage_logs')
op.drop_table('api_keys')
op.drop_table('translations')
op.drop_table('users')