112 lines
4 KiB
Python
112 lines
4 KiB
Python
"""api_keys + api_key_requests tables for B2B API (Stage 3).
|
|
|
|
Revision ID: 0002
|
|
Revises: 0001
|
|
Create Date: 2026-08-07
|
|
|
|
Hand-written (docs/ARCHITECTURE.md §7 + docs/TICKETS.md T-E3-001). status/rate columns are
|
|
TEXT+CHECK / INTEGER so later migrations remain additive.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
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 = "0002"
|
|
down_revision: str | None = "0001"
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ── api_keys ─────────────────────────────────────────────────────────────
|
|
op.create_table(
|
|
"api_keys",
|
|
sa.Column(
|
|
"id",
|
|
postgresql.UUID(as_uuid=True),
|
|
primary_key=True,
|
|
server_default=sa.text("gen_random_uuid()"),
|
|
),
|
|
sa.Column(
|
|
"user_id",
|
|
postgresql.UUID(as_uuid=True),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("name", sa.Text, nullable=False),
|
|
sa.Column("key_hash", sa.Text, nullable=False),
|
|
sa.Column("rate_limit_rps", sa.Integer, nullable=False, server_default=sa.text("3")),
|
|
sa.Column("monthly_quota", sa.Integer, nullable=True),
|
|
sa.Column("monthly_used", sa.Integer, nullable=False, server_default=sa.text("0")),
|
|
sa.Column(
|
|
"resets_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("(now() + interval '1 month')"),
|
|
),
|
|
sa.Column("revoked", sa.Boolean, nullable=False, server_default=sa.text("false")),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.func.now(),
|
|
),
|
|
sa.Column("last_used_at", sa.DateTime(timezone=True)),
|
|
sa.CheckConstraint("rate_limit_rps > 0", name="api_keys_rate_limit_positive"),
|
|
sa.CheckConstraint("monthly_used >= 0", name="api_keys_monthly_used_nonneg"),
|
|
sa.CheckConstraint(
|
|
"monthly_quota IS NULL OR monthly_quota >= 0",
|
|
name="api_keys_monthly_quota_nonneg",
|
|
),
|
|
sa.UniqueConstraint("user_id", "name", name="api_keys_user_name_unique"),
|
|
)
|
|
op.create_index("api_keys_user_idx", "api_keys", ["user_id"])
|
|
op.create_index("api_keys_hash_idx", "api_keys", ["key_hash"])
|
|
|
|
# ── api_key_requests ─────────────────────────────────────────────────────
|
|
op.create_table(
|
|
"api_key_requests",
|
|
sa.Column(
|
|
"id",
|
|
postgresql.UUID(as_uuid=True),
|
|
primary_key=True,
|
|
server_default=sa.text("gen_random_uuid()"),
|
|
),
|
|
sa.Column(
|
|
"api_key_id",
|
|
postgresql.UUID(as_uuid=True),
|
|
sa.ForeignKey("api_keys.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"document_id",
|
|
postgresql.UUID(as_uuid=True),
|
|
sa.ForeignKey("documents.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.func.now(),
|
|
),
|
|
)
|
|
op.create_index(
|
|
"api_key_requests_key_created_idx",
|
|
"api_key_requests",
|
|
["api_key_id", sa.text("created_at DESC")],
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("api_key_requests_key_created_idx", table_name="api_key_requests")
|
|
op.drop_table("api_key_requests")
|
|
op.drop_index("api_keys_hash_idx", table_name="api_keys")
|
|
op.drop_index("api_keys_user_idx", table_name="api_keys")
|
|
op.drop_table("api_keys")
|