forked from cardosofelipe/fast-next-template
Infrastructure: - Add Redis and Celery workers to all docker-compose files - Fix celery migration race condition in entrypoint.sh - Add healthchecks and resource limits to dev compose - Update .env.template with Redis/Celery variables Backend Models & Schemas: - Rename Sprint.completed_points to velocity (per requirements) - Add AgentInstance.name as required field - Rename Issue external tracker fields for consistency - Add IssueSource and TrackerType enums - Add Project.default_tracker_type field Backend Fixes: - Add Celery retry configuration with exponential backoff - Remove unused sequence counter from EventBus - Add mypy overrides for test dependencies - Fix test file using wrong schema (UserUpdate -> dict) Frontend Fixes: - Fix memory leak in useProjectEvents (proper cleanup) - Fix race condition with stale closure in reconnection - Sync TokenWithUser type with regenerated API client - Fix expires_in null handling in useAuth - Clean up unused imports in prototype pages - Add ESLint relaxed rules for prototype files CI/CD: - Add E2E testing stage with Testcontainers - Add security scanning with Trivy and pip-audit - Add dependency caching for faster builds Tests: - Update all tests to use renamed fields (velocity, name, etc.) - Fix 14 schema test failures - All 1500 tests pass with 91% coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
263 lines
17 KiB
Python
263 lines
17 KiB
Python
"""initial models
|
|
|
|
Revision ID: 0001
|
|
Revises:
|
|
Create Date: 2025-11-27 09:08:09.464506
|
|
|
|
"""
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = '0001'
|
|
down_revision: str | None = None
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.create_table('oauth_states',
|
|
sa.Column('state', sa.String(length=255), nullable=False),
|
|
sa.Column('code_verifier', sa.String(length=128), nullable=True),
|
|
sa.Column('nonce', sa.String(length=255), nullable=True),
|
|
sa.Column('provider', sa.String(length=50), nullable=False),
|
|
sa.Column('redirect_uri', sa.String(length=500), nullable=True),
|
|
sa.Column('user_id', sa.UUID(), nullable=True),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_oauth_states_state'), 'oauth_states', ['state'], unique=True)
|
|
op.create_table('organizations',
|
|
sa.Column('name', sa.String(length=255), nullable=False),
|
|
sa.Column('slug', sa.String(length=255), nullable=False),
|
|
sa.Column('description', sa.Text(), nullable=True),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('settings', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_organizations_is_active'), 'organizations', ['is_active'], unique=False)
|
|
op.create_index(op.f('ix_organizations_name'), 'organizations', ['name'], unique=False)
|
|
op.create_index('ix_organizations_name_active', 'organizations', ['name', 'is_active'], unique=False)
|
|
op.create_index(op.f('ix_organizations_slug'), 'organizations', ['slug'], unique=True)
|
|
op.create_index('ix_organizations_slug_active', 'organizations', ['slug', 'is_active'], unique=False)
|
|
op.create_table('users',
|
|
sa.Column('email', sa.String(length=255), nullable=False),
|
|
sa.Column('password_hash', sa.String(length=255), nullable=True),
|
|
sa.Column('first_name', sa.String(length=100), nullable=False),
|
|
sa.Column('last_name', sa.String(length=100), nullable=True),
|
|
sa.Column('phone_number', sa.String(length=20), nullable=True),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('is_superuser', sa.Boolean(), nullable=False),
|
|
sa.Column('preferences', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
|
sa.Column('locale', sa.String(length=10), nullable=True),
|
|
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_users_deleted_at'), 'users', ['deleted_at'], unique=False)
|
|
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
|
op.create_index(op.f('ix_users_is_active'), 'users', ['is_active'], unique=False)
|
|
op.create_index(op.f('ix_users_is_superuser'), 'users', ['is_superuser'], unique=False)
|
|
op.create_index(op.f('ix_users_locale'), 'users', ['locale'], unique=False)
|
|
op.create_table('oauth_accounts',
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('provider', sa.String(length=50), nullable=False),
|
|
sa.Column('provider_user_id', sa.String(length=255), nullable=False),
|
|
sa.Column('provider_email', sa.String(length=255), nullable=True),
|
|
sa.Column('access_token_encrypted', sa.String(length=2048), nullable=True),
|
|
sa.Column('refresh_token_encrypted', sa.String(length=2048), nullable=True),
|
|
sa.Column('token_expires_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id'),
|
|
sa.UniqueConstraint('provider', 'provider_user_id', name='uq_oauth_provider_user')
|
|
)
|
|
op.create_index(op.f('ix_oauth_accounts_provider'), 'oauth_accounts', ['provider'], unique=False)
|
|
op.create_index(op.f('ix_oauth_accounts_provider_email'), 'oauth_accounts', ['provider_email'], unique=False)
|
|
op.create_index(op.f('ix_oauth_accounts_user_id'), 'oauth_accounts', ['user_id'], unique=False)
|
|
op.create_index('ix_oauth_accounts_user_provider', 'oauth_accounts', ['user_id', 'provider'], unique=False)
|
|
op.create_table('oauth_clients',
|
|
sa.Column('client_id', sa.String(length=64), nullable=False),
|
|
sa.Column('client_secret_hash', sa.String(length=255), nullable=True),
|
|
sa.Column('client_name', sa.String(length=255), nullable=False),
|
|
sa.Column('client_description', sa.String(length=1000), nullable=True),
|
|
sa.Column('client_type', sa.String(length=20), nullable=False),
|
|
sa.Column('redirect_uris', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column('allowed_scopes', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
|
sa.Column('access_token_lifetime', sa.String(length=10), nullable=False),
|
|
sa.Column('refresh_token_lifetime', sa.String(length=10), nullable=False),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('owner_user_id', sa.UUID(), nullable=True),
|
|
sa.Column('mcp_server_url', sa.String(length=2048), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['owner_user_id'], ['users.id'], ondelete='SET NULL'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_oauth_clients_client_id'), 'oauth_clients', ['client_id'], unique=True)
|
|
op.create_index(op.f('ix_oauth_clients_is_active'), 'oauth_clients', ['is_active'], unique=False)
|
|
op.create_table('user_organizations',
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('organization_id', sa.UUID(), nullable=False),
|
|
sa.Column('role', sa.Enum('OWNER', 'ADMIN', 'MEMBER', 'GUEST', name='organizationrole'), nullable=False),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('custom_permissions', sa.String(length=500), nullable=True),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('user_id', 'organization_id')
|
|
)
|
|
op.create_index('ix_user_org_org_active', 'user_organizations', ['organization_id', 'is_active'], unique=False)
|
|
op.create_index('ix_user_org_role', 'user_organizations', ['role'], unique=False)
|
|
op.create_index('ix_user_org_user_active', 'user_organizations', ['user_id', 'is_active'], unique=False)
|
|
op.create_index(op.f('ix_user_organizations_is_active'), 'user_organizations', ['is_active'], unique=False)
|
|
op.create_table('user_sessions',
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('refresh_token_jti', sa.String(length=255), nullable=False),
|
|
sa.Column('device_name', sa.String(length=255), nullable=True),
|
|
sa.Column('device_id', sa.String(length=255), nullable=True),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
|
sa.Column('user_agent', sa.String(length=500), nullable=True),
|
|
sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('is_active', sa.Boolean(), nullable=False),
|
|
sa.Column('location_city', sa.String(length=100), nullable=True),
|
|
sa.Column('location_country', sa.String(length=100), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index(op.f('ix_user_sessions_is_active'), 'user_sessions', ['is_active'], unique=False)
|
|
op.create_index('ix_user_sessions_jti_active', 'user_sessions', ['refresh_token_jti', 'is_active'], unique=False)
|
|
op.create_index(op.f('ix_user_sessions_refresh_token_jti'), 'user_sessions', ['refresh_token_jti'], unique=True)
|
|
op.create_index('ix_user_sessions_user_active', 'user_sessions', ['user_id', 'is_active'], unique=False)
|
|
op.create_index(op.f('ix_user_sessions_user_id'), 'user_sessions', ['user_id'], unique=False)
|
|
op.create_table('oauth_authorization_codes',
|
|
sa.Column('code', sa.String(length=128), nullable=False),
|
|
sa.Column('client_id', sa.String(length=64), nullable=False),
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('redirect_uri', sa.String(length=2048), nullable=False),
|
|
sa.Column('scope', sa.String(length=1000), nullable=False),
|
|
sa.Column('code_challenge', sa.String(length=128), nullable=True),
|
|
sa.Column('code_challenge_method', sa.String(length=10), nullable=True),
|
|
sa.Column('state', sa.String(length=256), nullable=True),
|
|
sa.Column('nonce', sa.String(length=256), nullable=True),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('used', sa.Boolean(), nullable=False),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_oauth_authorization_codes_client_user', 'oauth_authorization_codes', ['client_id', 'user_id'], unique=False)
|
|
op.create_index(op.f('ix_oauth_authorization_codes_code'), 'oauth_authorization_codes', ['code'], unique=True)
|
|
op.create_index('ix_oauth_authorization_codes_expires_at', 'oauth_authorization_codes', ['expires_at'], unique=False)
|
|
op.create_table('oauth_consents',
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('client_id', sa.String(length=64), nullable=False),
|
|
sa.Column('granted_scopes', sa.String(length=1000), nullable=False),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_oauth_consents_user_client', 'oauth_consents', ['user_id', 'client_id'], unique=True)
|
|
op.create_table('oauth_provider_refresh_tokens',
|
|
sa.Column('token_hash', sa.String(length=64), nullable=False),
|
|
sa.Column('jti', sa.String(length=64), nullable=False),
|
|
sa.Column('client_id', sa.String(length=64), nullable=False),
|
|
sa.Column('user_id', sa.UUID(), nullable=False),
|
|
sa.Column('scope', sa.String(length=1000), nullable=False),
|
|
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('revoked', sa.Boolean(), nullable=False),
|
|
sa.Column('last_used_at', sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column('device_info', sa.String(length=500), nullable=True),
|
|
sa.Column('ip_address', sa.String(length=45), nullable=True),
|
|
sa.Column('id', sa.UUID(), nullable=False),
|
|
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
|
sa.ForeignKeyConstraint(['client_id'], ['oauth_clients.client_id'], ondelete='CASCADE'),
|
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
|
sa.PrimaryKeyConstraint('id')
|
|
)
|
|
op.create_index('ix_oauth_provider_refresh_tokens_client_user', 'oauth_provider_refresh_tokens', ['client_id', 'user_id'], unique=False)
|
|
op.create_index('ix_oauth_provider_refresh_tokens_expires_at', 'oauth_provider_refresh_tokens', ['expires_at'], unique=False)
|
|
op.create_index(op.f('ix_oauth_provider_refresh_tokens_jti'), 'oauth_provider_refresh_tokens', ['jti'], unique=True)
|
|
op.create_index(op.f('ix_oauth_provider_refresh_tokens_revoked'), 'oauth_provider_refresh_tokens', ['revoked'], unique=False)
|
|
op.create_index(op.f('ix_oauth_provider_refresh_tokens_token_hash'), 'oauth_provider_refresh_tokens', ['token_hash'], unique=True)
|
|
op.create_index('ix_oauth_provider_refresh_tokens_user_revoked', 'oauth_provider_refresh_tokens', ['user_id', 'revoked'], unique=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_index('ix_oauth_provider_refresh_tokens_user_revoked', table_name='oauth_provider_refresh_tokens')
|
|
op.drop_index(op.f('ix_oauth_provider_refresh_tokens_token_hash'), table_name='oauth_provider_refresh_tokens')
|
|
op.drop_index(op.f('ix_oauth_provider_refresh_tokens_revoked'), table_name='oauth_provider_refresh_tokens')
|
|
op.drop_index(op.f('ix_oauth_provider_refresh_tokens_jti'), table_name='oauth_provider_refresh_tokens')
|
|
op.drop_index('ix_oauth_provider_refresh_tokens_expires_at', table_name='oauth_provider_refresh_tokens')
|
|
op.drop_index('ix_oauth_provider_refresh_tokens_client_user', table_name='oauth_provider_refresh_tokens')
|
|
op.drop_table('oauth_provider_refresh_tokens')
|
|
op.drop_index('ix_oauth_consents_user_client', table_name='oauth_consents')
|
|
op.drop_table('oauth_consents')
|
|
op.drop_index('ix_oauth_authorization_codes_expires_at', table_name='oauth_authorization_codes')
|
|
op.drop_index(op.f('ix_oauth_authorization_codes_code'), table_name='oauth_authorization_codes')
|
|
op.drop_index('ix_oauth_authorization_codes_client_user', table_name='oauth_authorization_codes')
|
|
op.drop_table('oauth_authorization_codes')
|
|
op.drop_index(op.f('ix_user_sessions_user_id'), table_name='user_sessions')
|
|
op.drop_index('ix_user_sessions_user_active', table_name='user_sessions')
|
|
op.drop_index(op.f('ix_user_sessions_refresh_token_jti'), table_name='user_sessions')
|
|
op.drop_index('ix_user_sessions_jti_active', table_name='user_sessions')
|
|
op.drop_index(op.f('ix_user_sessions_is_active'), table_name='user_sessions')
|
|
op.drop_table('user_sessions')
|
|
op.drop_index(op.f('ix_user_organizations_is_active'), table_name='user_organizations')
|
|
op.drop_index('ix_user_org_user_active', table_name='user_organizations')
|
|
op.drop_index('ix_user_org_role', table_name='user_organizations')
|
|
op.drop_index('ix_user_org_org_active', table_name='user_organizations')
|
|
op.drop_table('user_organizations')
|
|
op.drop_index(op.f('ix_oauth_clients_is_active'), table_name='oauth_clients')
|
|
op.drop_index(op.f('ix_oauth_clients_client_id'), table_name='oauth_clients')
|
|
op.drop_table('oauth_clients')
|
|
op.drop_index('ix_oauth_accounts_user_provider', table_name='oauth_accounts')
|
|
op.drop_index(op.f('ix_oauth_accounts_user_id'), table_name='oauth_accounts')
|
|
op.drop_index(op.f('ix_oauth_accounts_provider_email'), table_name='oauth_accounts')
|
|
op.drop_index(op.f('ix_oauth_accounts_provider'), table_name='oauth_accounts')
|
|
op.drop_table('oauth_accounts')
|
|
op.drop_index(op.f('ix_users_locale'), table_name='users')
|
|
op.drop_index(op.f('ix_users_is_superuser'), table_name='users')
|
|
op.drop_index(op.f('ix_users_is_active'), table_name='users')
|
|
op.drop_index(op.f('ix_users_email'), table_name='users')
|
|
op.drop_index(op.f('ix_users_deleted_at'), table_name='users')
|
|
op.drop_table('users')
|
|
op.drop_index('ix_organizations_slug_active', table_name='organizations')
|
|
op.drop_index(op.f('ix_organizations_slug'), table_name='organizations')
|
|
op.drop_index('ix_organizations_name_active', table_name='organizations')
|
|
op.drop_index(op.f('ix_organizations_name'), table_name='organizations')
|
|
op.drop_index(op.f('ix_organizations_is_active'), table_name='organizations')
|
|
op.drop_table('organizations')
|
|
op.drop_index(op.f('ix_oauth_states_state'), table_name='oauth_states')
|
|
op.drop_table('oauth_states')
|
|
# ### end Alembic commands ###
|