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
This commit is contained in:
126
alembic/versions/001_initial.py
Normal file
126
alembic/versions/001_initial.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user