From 2b82f4acd8b5181f60ae4d0f2b343a076e0fa1a1 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:13:49 +0700 Subject: [PATCH] Alembic migration for synchronize Database --- alembic/versions/013_sync_role_permissions.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 alembic/versions/013_sync_role_permissions.py diff --git a/alembic/versions/013_sync_role_permissions.py b/alembic/versions/013_sync_role_permissions.py new file mode 100644 index 0000000..64ce6cc --- /dev/null +++ b/alembic/versions/013_sync_role_permissions.py @@ -0,0 +1,147 @@ +"""sync_role_permissions + +Revision ID: 013_sync_permissions +Revises: 012_fix_remaining +Create Date: 2026-01-05 + +Syncs role_permissions between DEV and PROD bidirectionally. +- Adds 18 DEV-only permissions to PROD (new features) +- Adds 6 PROD-only permissions to DEV (operational/security) +Result: Both environments have identical 142 permission mappings +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision: str = '013_sync_permissions' +down_revision: Union[str, None] = '012_fix_remaining' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Sync role_permissions bidirectionally""" + from sqlalchemy import text + + conn = op.get_bind() + + print("Syncing role_permissions between environments...") + + # ============================================================ + # STEP 1: Add missing permissions to ensure all exist + # ============================================================ + print("\n[1/2] Ensuring all permissions exist...") + + # Permissions that should exist (union of both environments) + all_permissions = [ + # From DEV-only list + ('donations.export', 'Export Donations', 'donations'), + ('donations.view', 'View Donations', 'donations'), + ('financials.create', 'Create Financial Reports', 'financials'), + ('financials.delete', 'Delete Financial Reports', 'financials'), + ('financials.edit', 'Edit Financial Reports', 'financials'), + ('financials.export', 'Export Financial Reports', 'financials'), + ('financials.payments', 'Manage Financial Payments', 'financials'), + ('settings.edit', 'Edit Settings', 'settings'), + ('settings.email_templates', 'Manage Email Templates', 'settings'), + ('subscriptions.activate', 'Activate Subscriptions', 'subscriptions'), + ('subscriptions.cancel', 'Cancel Subscriptions', 'subscriptions'), + ('subscriptions.create', 'Create Subscriptions', 'subscriptions'), + ('subscriptions.edit', 'Edit Subscriptions', 'subscriptions'), + ('subscriptions.export', 'Export Subscriptions', 'subscriptions'), + ('subscriptions.plans', 'Manage Subscription Plans', 'subscriptions'), + ('subscriptions.view', 'View Subscriptions', 'subscriptions'), + ('events.calendar_export', 'Export Event Calendar', 'events'), + ('events.rsvps', 'View Event RSVPs', 'events'), + # From PROD-only list + ('permissions.audit', 'Audit Permissions', 'permissions'), + ('permissions.view', 'View Permissions', 'permissions'), + ('settings.backup', 'Manage Backups', 'settings'), + ] + + for code, name, module in all_permissions: + # Insert if not exists + conn.execute(text(f""" + INSERT INTO permissions (id, code, name, description, module, created_at) + SELECT + gen_random_uuid(), + '{code}', + '{name}', + '{name}', + '{module}', + NOW() + WHERE NOT EXISTS ( + SELECT 1 FROM permissions WHERE code = '{code}' + ) + """)) + + print(" ✓ Ensured all permissions exist") + + # ============================================================ + # STEP 2: Add missing role-permission mappings + # ============================================================ + print("\n[2/2] Adding missing role-permission mappings...") + + # Mappings that should exist (union of both environments) + role_permission_mappings = [ + # DEV-only (add to PROD) + ('admin', 'donations.export'), + ('admin', 'donations.view'), + ('admin', 'financials.create'), + ('admin', 'financials.delete'), + ('admin', 'financials.edit'), + ('admin', 'financials.export'), + ('admin', 'financials.payments'), + ('admin', 'settings.edit'), + ('admin', 'settings.email_templates'), + ('admin', 'subscriptions.activate'), + ('admin', 'subscriptions.cancel'), + ('admin', 'subscriptions.create'), + ('admin', 'subscriptions.edit'), + ('admin', 'subscriptions.export'), + ('admin', 'subscriptions.plans'), + ('admin', 'subscriptions.view'), + ('member', 'events.calendar_export'), + ('member', 'events.rsvps'), + # PROD-only (add to DEV) + ('admin', 'permissions.audit'), + ('admin', 'permissions.view'), + ('admin', 'settings.backup'), + ('finance', 'bylaws.view'), + ('finance', 'events.view'), + ('finance', 'newsletters.view'), + ] + + added_count = 0 + for role, perm_code in role_permission_mappings: + result = conn.execute(text(f""" + INSERT INTO role_permissions (id, role, permission_id, created_at) + SELECT + gen_random_uuid(), + '{role}', + p.id, + NOW() + FROM permissions p + WHERE p.code = '{perm_code}' + AND NOT EXISTS ( + SELECT 1 FROM role_permissions rp + WHERE rp.role = '{role}' + AND rp.permission_id = p.id + ) + RETURNING id + """)) + if result.rowcount > 0: + added_count += 1 + + print(f" ✓ Added {added_count} missing role-permission mappings") + + # Verify final count + final_count = conn.execute(text("SELECT COUNT(*) FROM role_permissions")).scalar() + print(f"\n✅ Role-permission mappings synchronized: {final_count} total") + + +def downgrade() -> None: + """Revert sync (not recommended)""" + print("⚠️ Downgrade not supported - permissions are additive") + pass