"""users and connections schema Revision ID: 0002 Revises: 0001 Create Date: 2026-04-28 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "0002" down_revision: Union[str, None] = "0001" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: conn = op.get_bind() # ── users ──────────────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL, phone VARCHAR(20) NOT NULL, password_hash VARCHAR(255) NULL, is_email_confirmed BOOLEAN NOT NULL DEFAULT FALSE, email_confirm_token VARCHAR(255) NULL, password_reset_token VARCHAR(255) NULL, password_reset_expires DATETIME NULL, role ENUM('system','admin','user') NOT NULL DEFAULT 'user', status ENUM('pending','active','suspended') NOT NULL DEFAULT 'pending', evotor_user_id VARCHAR(255) NULL, evotor_meta JSON NULL, invite_token VARCHAR(255) NULL, invite_expires DATETIME NULL, phone_otp VARCHAR(10) NULL, phone_otp_expires DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT ix_users_email UNIQUE (email), CONSTRAINT ix_users_phone UNIQUE (phone), CONSTRAINT ix_users_evotor_user_id UNIQUE (evotor_user_id) ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) # Add new columns if migrating from the Node.js schema (which lacked them) for col_sql in [ "ALTER TABLE users MODIFY COLUMN password_hash VARCHAR(255) NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS role ENUM('system','admin','user') NOT NULL DEFAULT 'user'", "ALTER TABLE users ADD COLUMN IF NOT EXISTS status ENUM('pending','active','suspended') NOT NULL DEFAULT 'pending'", "ALTER TABLE users ADD COLUMN IF NOT EXISTS evotor_user_id VARCHAR(255) NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS evotor_meta JSON NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS invite_token VARCHAR(255) NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS invite_expires DATETIME NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_otp VARCHAR(10) NULL", "ALTER TABLE users ADD COLUMN IF NOT EXISTS phone_otp_expires DATETIME NULL", ]: try: conn.execute(sa.text(col_sql)) except Exception: pass # column/constraint already exists # Add unique index on evotor_user_id if missing try: conn.execute(sa.text( "ALTER TABLE users ADD CONSTRAINT ix_users_evotor_user_id UNIQUE (evotor_user_id)" )) except Exception: pass # Add role/status indexes if missing for idx_sql in [ "CREATE INDEX IF NOT EXISTS ix_users_role ON users (role)", "CREATE INDEX IF NOT EXISTS ix_users_status ON users (status)", ]: try: conn.execute(sa.text(idx_sql)) except Exception: pass # ── evotor_connections ─────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS evotor_connections ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NULL, evotor_user_id VARCHAR(255) NULL, access_token TEXT NOT NULL, api_token VARCHAR(255) NULL, store_id VARCHAR(255) NULL, store_name VARCHAR(255) NULL, refresh_token TEXT NULL, token_expires_at DATETIME NULL, is_online BOOLEAN NOT NULL DEFAULT FALSE, last_checked_at DATETIME NULL, connected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT ix_evotor_connections_user_id UNIQUE (user_id), CONSTRAINT ix_evotor_connections_evotor_user_id UNIQUE (evotor_user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) try: conn.execute(sa.text( "ALTER TABLE evotor_connections ADD COLUMN IF NOT EXISTS api_token VARCHAR(255) NULL" )) except Exception: pass # ── vk_connections ─────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS vk_connections ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, access_token TEXT NOT NULL, vk_user_id VARCHAR(50) NULL, first_name VARCHAR(255) NULL, last_name VARCHAR(255) NULL, is_online BOOLEAN NOT NULL DEFAULT FALSE, last_checked_at DATETIME NULL, connected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT ix_vk_connections_user_id UNIQUE (user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) # ── sync_configs ───────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS sync_configs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, is_enabled BOOLEAN NOT NULL DEFAULT FALSE, confirmed_at DATETIME NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT ix_sync_configs_user_id UNIQUE (user_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) # ── sync_filters ───────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS sync_filters ( id INT AUTO_INCREMENT PRIMARY KEY, sync_config_id INT NOT NULL, entity_type VARCHAR(20) NOT NULL, entity_id VARCHAR(255) NOT NULL, entity_name VARCHAR(255) NULL, filter_mode VARCHAR(10) NOT NULL, parent_entity_id VARCHAR(255) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_sync_filters_config_type_entity UNIQUE (sync_config_id, entity_type, entity_id), FOREIGN KEY (sync_config_id) REFERENCES sync_configs(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) # ── cached_stores ──────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS cached_stores ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, evotor_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, address VARCHAR(500) NULL, fetched_at DATETIME NOT NULL, CONSTRAINT uq_cached_stores_user_evotor UNIQUE (user_id, evotor_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) try: conn.execute(sa.text("CREATE INDEX IF NOT EXISTS ix_cached_stores_user_id ON cached_stores (user_id)")) except Exception: pass # ── cached_groups ──────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS cached_groups ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, evotor_id VARCHAR(255) NOT NULL, store_evotor_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, fetched_at DATETIME NOT NULL, CONSTRAINT uq_cached_groups_user_evotor UNIQUE (user_id, evotor_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) try: conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_cached_groups_user_store ON cached_groups (user_id, store_evotor_id)" )) except Exception: pass # ── cached_products ────────────────────────────────────────────────────── conn.execute(sa.text(""" CREATE TABLE IF NOT EXISTS cached_products ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, evotor_id VARCHAR(255) NOT NULL, store_evotor_id VARCHAR(255) NOT NULL, group_evotor_id VARCHAR(255) NULL, name VARCHAR(255) NOT NULL, price DECIMAL(12,2) NULL, quantity DECIMAL(12,3) NULL, measure_name VARCHAR(20) NULL, article_number VARCHAR(100) NULL, allow_to_sell BOOLEAN NULL, fetched_at DATETIME NOT NULL, synced_at DATETIME NULL, CONSTRAINT uq_cached_products_user_evotor UNIQUE (user_id, evotor_id), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci """)) try: conn.execute(sa.text( "CREATE INDEX IF NOT EXISTS ix_cached_products_user_store_group " "ON cached_products (user_id, store_evotor_id, group_evotor_id)" )) except Exception: pass def downgrade() -> None: conn = op.get_bind() for table in [ "cached_products", "cached_groups", "cached_stores", "sync_filters", "sync_configs", "vk_connections", "evotor_connections", "users", ]: conn.execute(sa.text(f"DROP TABLE IF EXISTS {table}"))