diff --git a/migrations/versions/038_add_api_token_expires_at.py b/migrations/versions/038_add_api_token_expires_at.py new file mode 100644 index 00000000..20d932aa --- /dev/null +++ b/migrations/versions/038_add_api_token_expires_at.py @@ -0,0 +1,43 @@ +"""Add expires_at column to api_tokens table. + +Allows API tokens to be issued with an optional lifetime. If ``expires_at`` +is set, the token is automatically rejected after that timestamp. + +Revision ID: 038_add_api_token_expires_at +Revises: 037_add_user_sessions_and_qr_challenges +Create Date: 2026-03-18 +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "038_add_api_token_expires_at" +down_revision: Union[str, None] = "037_add_user_sessions_and_qr_challenges" +depends_on: Union[str, None] = None + + +def upgrade() -> None: + """Add expires_at column to api_tokens (idempotent).""" + conn = op.get_bind() + inspector = sa.inspect(conn) + if "api_tokens" not in inspector.get_table_names(): + return + existing_columns = {col["name"] for col in inspector.get_columns("api_tokens")} + if "expires_at" not in existing_columns: + op.add_column( + "api_tokens", + sa.Column("expires_at", sa.DateTime(timezone=True), nullable=True), + ) + + +def downgrade() -> None: + """Remove expires_at column from api_tokens.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + if "api_tokens" not in inspector.get_table_names(): + return + existing_columns = {col["name"] for col in inspector.get_columns("api_tokens")} + if "expires_at" in existing_columns: + op.drop_column("api_tokens", "expires_at") diff --git a/migrations/versions/039_add_classification_rules.py b/migrations/versions/039_add_classification_rules.py new file mode 100644 index 00000000..66e1aaf2 --- /dev/null +++ b/migrations/versions/039_add_classification_rules.py @@ -0,0 +1,46 @@ +"""Add classification_rules table for custom document classification rules. + +Revision ID: 039_add_classification_rules +Revises: 038_add_api_token_expires_at +Create Date: 2026-03-17 +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "039_add_classification_rules" +down_revision: Union[str, None] = "038_add_api_token_expires_at" +depends_on: Union[str, None] = None + + +def upgrade() -> None: + """Create classification_rules table.""" + op.create_table( + "classification_rules", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("owner_id", sa.String(), nullable=True), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("category", sa.String(100), nullable=False), + sa.Column("rule_type", sa.String(50), nullable=False), + sa.Column("pattern", sa.String(1000), nullable=False), + sa.Column("priority", sa.Integer(), nullable=False, server_default="0"), + sa.Column("case_sensitive", sa.Boolean(), nullable=False, server_default="0"), + sa.Column("enabled", sa.Boolean(), nullable=False, server_default="1"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("owner_id", "name", name="uq_classification_rules_owner_name"), + ) + op.create_index("ix_classification_rules_id", "classification_rules", ["id"]) + op.create_index("ix_classification_rules_owner_id", "classification_rules", ["owner_id"]) + op.create_index("ix_classification_rules_category", "classification_rules", ["category"]) + + +def downgrade() -> None: + """Drop classification_rules table.""" + op.drop_index("ix_classification_rules_category", "classification_rules") + op.drop_index("ix_classification_rules_owner_id", "classification_rules") + op.drop_index("ix_classification_rules_id", "classification_rules") + op.drop_table("classification_rules") diff --git a/migrations/versions/040_add_automation_hooks.py b/migrations/versions/040_add_automation_hooks.py new file mode 100644 index 00000000..3d9b778c --- /dev/null +++ b/migrations/versions/040_add_automation_hooks.py @@ -0,0 +1,39 @@ +"""Add automation_hooks table for Zapier / Make.com webhook subscriptions. + +Revision ID: 040_add_automation_hooks +Revises: 039_add_classification_rules +Create Date: 2026-03-09 +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "040_add_automation_hooks" +down_revision: Union[str, None] = "039_add_classification_rules" +depends_on: Union[str, None] = None + + +def upgrade() -> None: + """Create automation_hooks table.""" + op.create_table( + "automation_hooks", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("target_url", sa.String(), nullable=False), + sa.Column("secret", sa.String(), nullable=True), + sa.Column("events", sa.Text(), nullable=False), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default="1"), + sa.Column("hook_type", sa.String(50), nullable=False, server_default="generic"), + sa.Column("description", sa.String(), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_automation_hooks_id", "automation_hooks", ["id"]) + + +def downgrade() -> None: + """Drop automation_hooks table.""" + op.drop_index("ix_automation_hooks_id", "automation_hooks") + op.drop_table("automation_hooks") diff --git a/migrations/versions/041_add_document_comments_and_annotations.py b/migrations/versions/041_add_document_comments_and_annotations.py new file mode 100644 index 00000000..3bb84339 --- /dev/null +++ b/migrations/versions/041_add_document_comments_and_annotations.py @@ -0,0 +1,88 @@ +"""Add document_comments and document_annotations tables. + +Revision ID: 041_add_document_comments_and_annotations +Revises: 040_add_automation_hooks +Create Date: 2026-03-21 + +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "041_add_document_comments_and_annotations" +down_revision: Union[str, None] = "040_add_automation_hooks" +depends_on: Union[str, None] = None + + +def upgrade() -> None: + """Add document_comments and document_annotations tables.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if "document_comments" not in existing_tables: + op.create_table( + "document_comments", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("file_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("parent_id", sa.Integer(), nullable=True), + sa.Column("body", sa.Text(), nullable=False), + sa.Column("mentions", sa.Text(), nullable=True), + sa.Column("is_resolved", sa.Boolean(), nullable=False, server_default="0"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()), + sa.ForeignKeyConstraint(["file_id"], ["files.id"]), + sa.ForeignKeyConstraint(["parent_id"], ["document_comments.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_document_comments_id", "document_comments", ["id"]) + op.create_index("ix_document_comments_file_id", "document_comments", ["file_id"]) + op.create_index("ix_document_comments_user_id", "document_comments", ["user_id"]) + op.create_index("ix_document_comments_parent_id", "document_comments", ["parent_id"]) + + if "document_annotations" not in existing_tables: + op.create_table( + "document_annotations", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("file_id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.String(), nullable=False), + sa.Column("page", sa.Integer(), nullable=False), + sa.Column("x", sa.Float(), nullable=False), + sa.Column("y", sa.Float(), nullable=False), + sa.Column("width", sa.Float(), nullable=False), + sa.Column("height", sa.Float(), nullable=False), + sa.Column("content", sa.Text(), nullable=False), + sa.Column("annotation_type", sa.String(50), nullable=False, server_default="note"), + sa.Column("color", sa.String(20), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now()), + sa.ForeignKeyConstraint(["file_id"], ["files.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_document_annotations_id", "document_annotations", ["id"]) + op.create_index("ix_document_annotations_file_id", "document_annotations", ["file_id"]) + op.create_index("ix_document_annotations_user_id", "document_annotations", ["user_id"]) + + +def downgrade() -> None: + """Drop document_comments and document_annotations tables.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if "document_annotations" in existing_tables: + op.drop_index("ix_document_annotations_user_id", "document_annotations") + op.drop_index("ix_document_annotations_file_id", "document_annotations") + op.drop_index("ix_document_annotations_id", "document_annotations") + op.drop_table("document_annotations") + + if "document_comments" in existing_tables: + op.drop_index("ix_document_comments_parent_id", "document_comments") + op.drop_index("ix_document_comments_user_id", "document_comments") + op.drop_index("ix_document_comments_file_id", "document_comments") + op.drop_index("ix_document_comments_id", "document_comments") + op.drop_table("document_comments") diff --git a/migrations/versions/042_add_file_shares.py b/migrations/versions/042_add_file_shares.py new file mode 100644 index 00000000..5ff73c3a --- /dev/null +++ b/migrations/versions/042_add_file_shares.py @@ -0,0 +1,58 @@ +"""Add file_shares table for per-user document sharing and role-based access. + +Revision ID: 042_add_file_shares +Revises: 041_add_document_comments_and_annotations +Create Date: 2026-03-22 + +""" + +from typing import Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "042_add_file_shares" +down_revision: Union[str, None] = "041_add_document_comments_and_annotations" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Create file_shares table.""" + conn = op.get_bind() + inspector = sa.inspect(conn) + existing_tables = set(inspector.get_table_names()) + + if "file_shares" not in existing_tables: + op.create_table( + "file_shares", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("file_id", sa.Integer(), nullable=False), + sa.Column("owner_id", sa.String(), nullable=False), + sa.Column("shared_with_user_id", sa.String(), nullable=False), + sa.Column("role", sa.String(20), nullable=False, server_default="viewer"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + server_default=sa.func.now(), + onupdate=sa.func.now(), + ), + sa.ForeignKeyConstraint(["file_id"], ["files.id"]), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("file_id", "shared_with_user_id", name="uq_file_share_file_user"), + ) + op.create_index("ix_file_shares_id", "file_shares", ["id"]) + op.create_index("ix_file_shares_file_id", "file_shares", ["file_id"]) + op.create_index("ix_file_shares_owner_id", "file_shares", ["owner_id"]) + op.create_index("ix_file_shares_shared_with_user_id", "file_shares", ["shared_with_user_id"]) + + +def downgrade() -> None: + """Drop file_shares table.""" + op.drop_index("ix_file_shares_shared_with_user_id", table_name="file_shares") + op.drop_index("ix_file_shares_owner_id", table_name="file_shares") + op.drop_index("ix_file_shares_file_id", table_name="file_shares") + op.drop_index("ix_file_shares_id", table_name="file_shares") + op.drop_table("file_shares")