fix(migrations): merge main and rechain compliance migration as 031 after 030_add_mobile_devices

Merge main branch into compliance templates feature branch.

Main had advanced with migrations 027-030 (ensure_shared_links,
audit_logs, user_language_preference, mobile_devices) since this
branch forked. Our compliance migration was 027 with down_revision
026, which conflicted with main's 027_ensure_shared_links_table.

Changes:
- Merge main (including i18n, audit logs, mobile, GraphQL features)
- Resolve conflicts in app/api/__init__.py, app/models.py, tests/conftest.py
- Rename 027_add_compliance_templates → 031_add_compliance_templates
- Rechain: down_revision 026_add_scheduled_jobs → 030_add_mobile_devices
- Add ComplianceTemplate to migrations/env.py imports
- Alembic now has single head: 031_add_compliance_templates
This commit is contained in:
copilot-swe-agent[bot]
2026-03-12 00:00:29 +00:00
94 changed files with 23120 additions and 98 deletions
+18
View File
@@ -20,13 +20,31 @@ from app.database import Base
# Ensure all models are imported so Base.metadata is populated.
from app.models import ( # noqa: F401
ApiToken,
ApplicationSettings,
AuditLog,
BackupRecord,
ComplianceTemplate,
DocumentMetadata,
FileProcessingStep,
FileRecord,
InAppNotification,
LocalUser,
MobileDevice,
Pipeline,
PipelineStep,
ProcessingLog,
SavedSearch,
ScheduledJob,
SettingsAuditLog,
SharedLink,
SubscriptionPlan,
UserImapAccount,
UserIntegration,
UserNotificationPreference,
UserNotificationTarget,
UserProfile,
WebhookConfig,
)
# Alembic Config object provides access to values in alembic.ini.
@@ -0,0 +1,62 @@
"""Ensure shared_links table exists for databases that skipped migration 025.
Databases that were already at revision 025_add_user_notifications or
026_add_scheduled_jobs before 025_add_shared_links was inserted into the
migration chain will never have had the ``shared_links`` table created.
This migration creates the table idempotently so those databases are
repaired on the next ``alembic upgrade head``.
Revision ID: 027_ensure_shared_links_table
Revises: 026_add_scheduled_jobs
Create Date: 2026-03-09
"""
from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "027_ensure_shared_links_table"
down_revision: Union[str, None] = "026_add_scheduled_jobs"
depends_on: Union[str, None] = None
def upgrade() -> None:
"""Create shared_links table if it does not already exist."""
conn = op.get_bind()
inspector = sa.inspect(conn)
if "shared_links" not in inspector.get_table_names():
op.create_table(
"shared_links",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("token", sa.String(64), nullable=False),
sa.Column("file_id", sa.Integer(), nullable=False),
sa.Column("owner_id", sa.String(), nullable=False),
sa.Column("label", sa.String(255), nullable=True),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("max_views", sa.Integer(), nullable=True),
sa.Column("view_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("password_hash", sa.String(128), nullable=True),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(["file_id"], ["files.id"]),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("token"),
)
op.create_index("ix_shared_links_id", "shared_links", ["id"])
op.create_index("ix_shared_links_token", "shared_links", ["token"])
op.create_index("ix_shared_links_file_id", "shared_links", ["file_id"])
op.create_index("ix_shared_links_owner_id", "shared_links", ["owner_id"])
def downgrade() -> None:
"""Drop shared_links table only if this migration created it."""
conn = op.get_bind()
inspector = sa.inspect(conn)
if "shared_links" in inspector.get_table_names():
op.drop_index("ix_shared_links_owner_id", "shared_links")
op.drop_index("ix_shared_links_file_id", "shared_links")
op.drop_index("ix_shared_links_token", "shared_links")
op.drop_index("ix_shared_links_id", "shared_links")
op.drop_table("shared_links")
+47
View File
@@ -0,0 +1,47 @@
"""Add audit_logs table for comprehensive compliance audit logging.
Revision ID: 028_add_audit_logs
Revises: 027_ensure_shared_links_table
Create Date: 2026-03-09
"""
from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "028_add_audit_logs"
down_revision: Union[str, None] = "027_ensure_shared_links_table"
depends_on: Union[str, None] = None
def upgrade() -> None:
"""Create audit_logs table."""
op.create_table(
"audit_logs",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("timestamp", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("user", sa.String(), nullable=False),
sa.Column("action", sa.String(), nullable=False),
sa.Column("resource_type", sa.String(), nullable=True),
sa.Column("resource_id", sa.String(), nullable=True),
sa.Column("ip_address", sa.String(), nullable=True),
sa.Column("details", sa.Text(), nullable=True),
sa.Column("severity", sa.String(16), nullable=False, server_default="info"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_audit_logs_id", "audit_logs", ["id"])
op.create_index("ix_audit_logs_timestamp", "audit_logs", ["timestamp"])
op.create_index("ix_audit_logs_user", "audit_logs", ["user"])
op.create_index("ix_audit_logs_action", "audit_logs", ["action"])
op.create_index("ix_audit_logs_resource_type", "audit_logs", ["resource_type"])
def downgrade() -> None:
"""Drop audit_logs table."""
op.drop_index("ix_audit_logs_resource_type", "audit_logs")
op.drop_index("ix_audit_logs_action", "audit_logs")
op.drop_index("ix_audit_logs_user", "audit_logs")
op.drop_index("ix_audit_logs_timestamp", "audit_logs")
op.drop_index("ix_audit_logs_id", "audit_logs")
op.drop_table("audit_logs")
@@ -0,0 +1,40 @@
"""Add preferred_language column to user_profiles for i18n support.
Revision ID: 029_add_user_language_preference
Revises: 028_add_audit_logs
Create Date: 2026-03-09
"""
from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "029_add_user_language_preference"
down_revision: Union[str, None] = "028_add_audit_logs"
depends_on: Union[str, None] = None
def upgrade() -> None:
"""Add preferred_language column to user_profiles table."""
conn = op.get_bind()
inspector = sa.inspect(conn)
if "user_profiles" not in inspector.get_table_names():
return
existing_columns = {col["name"] for col in inspector.get_columns("user_profiles")}
if "preferred_language" not in existing_columns:
op.add_column(
"user_profiles",
sa.Column("preferred_language", sa.String(10), nullable=True, server_default=None),
)
def downgrade() -> None:
"""Remove preferred_language column from user_profiles table."""
conn = op.get_bind()
inspector = sa.inspect(conn)
if "user_profiles" not in inspector.get_table_names():
return
existing_columns = {col["name"] for col in inspector.get_columns("user_profiles")}
if "preferred_language" in existing_columns:
op.drop_column("user_profiles", "preferred_language")
@@ -0,0 +1,41 @@
"""Add mobile_devices table for push notification device registration.
Revision ID: 030_add_mobile_devices
Revises: 029_add_user_language_preference
Create Date: 2026-03-10
"""
from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "030_add_mobile_devices"
down_revision: Union[str, None] = "029_add_user_language_preference"
depends_on: Union[str, None] = None
def upgrade() -> None:
"""Create mobile_devices table."""
op.create_table(
"mobile_devices",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("owner_id", sa.String(), nullable=False),
sa.Column("device_name", sa.String(255), nullable=True),
sa.Column("platform", sa.String(20), nullable=False, server_default="ios"),
sa.Column("push_token", sa.String(512), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("owner_id", "push_token", name="uq_mobile_device_owner_token"),
)
op.create_index("ix_mobile_devices_id", "mobile_devices", ["id"])
op.create_index("ix_mobile_devices_owner_id", "mobile_devices", ["owner_id"])
def downgrade() -> None:
"""Drop mobile_devices table."""
op.drop_index("ix_mobile_devices_owner_id", table_name="mobile_devices")
op.drop_index("ix_mobile_devices_id", table_name="mobile_devices")
op.drop_table("mobile_devices")
@@ -1,7 +1,7 @@
"""Add compliance_templates table for GDPR, HIPAA, SOC2 compliance templates.
Revision ID: 027_add_compliance_templates
Revises: 026_add_scheduled_jobs
Revision ID: 031_add_compliance_templates
Revises: 030_add_mobile_devices
Create Date: 2026-03-09
"""
@@ -10,8 +10,8 @@ from typing import Union
import sqlalchemy as sa
from alembic import op
revision: str = "027_add_compliance_templates"
down_revision: Union[str, None] = "026_add_scheduled_jobs"
revision: str = "031_add_compliance_templates"
down_revision: Union[str, None] = "030_add_mobile_devices"
depends_on: Union[str, None] = None