Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy import pool
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from app import models # noqa: F401 (registers all model modules on Base.metadata)
|
||||
from app.core.config import get_settings
|
||||
from app.db.base import Base
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
settings = get_settings()
|
||||
config.set_main_option("sqlalchemy.url", settings.database_url)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection: Connection) -> None:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
@@ -0,0 +1,27 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: str | None = ${repr(down_revision)}
|
||||
branch_labels: Sequence[str] | str | None = ${repr(branch_labels)}
|
||||
depends_on: Sequence[str] | str | None = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
||||
@@ -0,0 +1,64 @@
|
||||
"""create users and refresh tokens
|
||||
|
||||
Revision ID: 05a3ccfe49b9
|
||||
Revises:
|
||||
Create Date: 2026-07-31 18:14:41.012285
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "05a3ccfe49b9"
|
||||
down_revision: str | None = None
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("email", sa.String(length=320), nullable=False),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=True),
|
||||
sa.Column("display_name", sa.String(length=120), nullable=False),
|
||||
sa.Column("timezone", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("is_admin", sa.Boolean(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||
op.create_table(
|
||||
"refresh_tokens",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_refresh_tokens_token_hash"), "refresh_tokens", ["token_hash"], unique=True
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_refresh_tokens_token_hash"), table_name="refresh_tokens")
|
||||
op.drop_table("refresh_tokens")
|
||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||
op.drop_table("users")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,57 @@
|
||||
"""add per-source scheduling override columns
|
||||
|
||||
Revision ID: 06e7f03cea36
|
||||
Revises: 60a25ddfc6a3
|
||||
Create Date: 2026-08-03 17:22:37.278775
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "06e7f03cea36"
|
||||
down_revision: str | None = "60a25ddfc6a3"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"sources",
|
||||
sa.Column(
|
||||
"frequency_type",
|
||||
sa.Enum(
|
||||
"HOURLY",
|
||||
"EVERY_6_HOURS",
|
||||
"EVERY_12_HOURS",
|
||||
"DAILY",
|
||||
"EVERY_2_DAYS",
|
||||
"WEEKLY",
|
||||
"EVERY_2_WEEKS",
|
||||
"MONTHLY",
|
||||
"CUSTOM",
|
||||
name="monitoringfrequency",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.add_column("sources", sa.Column("interval_minutes", sa.Integer(), nullable=True))
|
||||
op.add_column("sources", sa.Column("cron_expression", sa.String(length=120), nullable=True))
|
||||
op.add_column("sources", sa.Column("next_check", sa.DateTime(timezone=True), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("sources", "next_check")
|
||||
op.drop_column("sources", "cron_expression")
|
||||
op.drop_column("sources", "interval_minutes")
|
||||
op.drop_column("sources", "frequency_type")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,155 @@
|
||||
"""create sources source_documents and snapshots
|
||||
|
||||
Revision ID: 484419ccd357
|
||||
Revises: 8880af3b8aa1
|
||||
Create Date: 2026-07-31 18:55:01.926914
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "484419ccd357"
|
||||
down_revision: str | None = "8880af3b8aa1"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"sources",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"source_type",
|
||||
sa.Enum(
|
||||
"WEBSITE",
|
||||
"RSS",
|
||||
"CUSTOM_URL",
|
||||
"SEC_EDGAR",
|
||||
"GITHUB",
|
||||
"JOB_POSTING",
|
||||
"PATENT",
|
||||
"REVIEW",
|
||||
name="sourcetype",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column("base_url", sa.String(length=500), nullable=True),
|
||||
sa.Column("active", sa.Boolean(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"ACTIVE",
|
||||
"DISABLED",
|
||||
"RATE_LIMITED",
|
||||
"AUTH_REQUIRED",
|
||||
"BLOCKED_BY_POLICY",
|
||||
"FAILED",
|
||||
name="sourcestatus",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("trust_score", sa.Float(), nullable=False),
|
||||
sa.Column("last_checked", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("last_successful_check", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("failure_count", sa.Integer(), nullable=False),
|
||||
sa.Column("configuration_metadata", sa.JSON(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_sources_company_id"), "sources", ["company_id"], unique=False)
|
||||
op.create_table(
|
||||
"snapshots",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("source_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("snapshot_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("structured_summary", sa.JSON(), nullable=False),
|
||||
sa.Column("text_summary", sa.Text(), nullable=True),
|
||||
sa.Column("monitoring_run_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_snapshots_company_id"), "snapshots", ["company_id"], unique=False)
|
||||
op.create_index(op.f("ix_snapshots_hash"), "snapshots", ["hash"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_snapshots_monitoring_run_id"), "snapshots", ["monitoring_run_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_snapshots_source_id"), "snapshots", ["source_id"], unique=False)
|
||||
op.create_table(
|
||||
"source_documents",
|
||||
sa.Column("source_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("url", sa.String(length=1000), nullable=False),
|
||||
sa.Column("canonical_url", sa.String(length=1000), nullable=False),
|
||||
sa.Column("title", sa.String(length=500), nullable=True),
|
||||
sa.Column("author", sa.String(length=200), nullable=True),
|
||||
sa.Column("publication_date", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("retrieved_date", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("content_text", sa.Text(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("metadata_json", sa.JSON(), nullable=False),
|
||||
sa.Column("language", sa.String(length=16), nullable=True),
|
||||
sa.Column("http_status", sa.Integer(), nullable=True),
|
||||
sa.Column("extraction_method", sa.String(length=50), nullable=False),
|
||||
sa.Column("trust_score", sa.Float(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_source_documents_canonical_url"),
|
||||
"source_documents",
|
||||
["canonical_url"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_source_documents_company_id"), "source_documents", ["company_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_source_documents_content_hash"), "source_documents", ["content_hash"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_source_documents_source_id"), "source_documents", ["source_id"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_source_documents_source_id"), table_name="source_documents")
|
||||
op.drop_index(op.f("ix_source_documents_content_hash"), table_name="source_documents")
|
||||
op.drop_index(op.f("ix_source_documents_company_id"), table_name="source_documents")
|
||||
op.drop_index(op.f("ix_source_documents_canonical_url"), table_name="source_documents")
|
||||
op.drop_table("source_documents")
|
||||
op.drop_index(op.f("ix_snapshots_source_id"), table_name="snapshots")
|
||||
op.drop_index(op.f("ix_snapshots_monitoring_run_id"), table_name="snapshots")
|
||||
op.drop_index(op.f("ix_snapshots_hash"), table_name="snapshots")
|
||||
op.drop_index(op.f("ix_snapshots_company_id"), table_name="snapshots")
|
||||
op.drop_table("snapshots")
|
||||
op.drop_index(op.f("ix_sources_company_id"), table_name="sources")
|
||||
op.drop_table("sources")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,120 @@
|
||||
"""add notification_destination_companies join table
|
||||
|
||||
Notification destinations were previously user-global with no concept of
|
||||
"which company is this for" - every destination got every one of the user's
|
||||
companies' alerts. This migration adds the join table and then, in the same
|
||||
step, (1) backfills links so every existing destination is linked to every
|
||||
company the same user currently has (this is a no-op behavior change - it's
|
||||
exactly what already happened implicitly before this table existed), and (2)
|
||||
deduplicates destinations that share the same (user, type, value) - the
|
||||
wizard previously created a brand new row per company even when the same
|
||||
email/phone was reused, which is the duplicate-rows bug this whole feature
|
||||
was built to fix. The earliest-created row per duplicate group is kept;
|
||||
later duplicates are deleted (cascading to their NotificationDelivery
|
||||
history, which is acceptable one-time cleanup - see KNOWN_LIMITATIONS.md).
|
||||
|
||||
Revision ID: 60a25ddfc6a3
|
||||
Revises: f01919a99ee9
|
||||
Create Date: 2026-08-02 11:47:04.760190
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "60a25ddfc6a3"
|
||||
down_revision: str | None = "f01919a99ee9"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"notification_destination_companies",
|
||||
sa.Column("destination_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["destination_id"], ["notification_destinations.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("destination_id", "company_id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
_backfill_links_and_dedupe_destinations()
|
||||
|
||||
|
||||
def _backfill_links_and_dedupe_destinations() -> None:
|
||||
bind = op.get_bind()
|
||||
|
||||
companies_t = sa.table(
|
||||
"companies",
|
||||
sa.column("id", sa.Uuid()),
|
||||
sa.column("user_id", sa.Uuid()),
|
||||
)
|
||||
destinations_t = sa.table(
|
||||
"notification_destinations",
|
||||
sa.column("id", sa.Uuid()),
|
||||
sa.column("user_id", sa.Uuid()),
|
||||
sa.column("type", sa.String()),
|
||||
sa.column("destination_value", sa.String()),
|
||||
sa.column("created_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
links_t = sa.table(
|
||||
"notification_destination_companies",
|
||||
sa.column("destination_id", sa.Uuid()),
|
||||
sa.column("company_id", sa.Uuid()),
|
||||
sa.column("created_at", sa.DateTime(timezone=True)),
|
||||
sa.column("updated_at", sa.DateTime(timezone=True)),
|
||||
)
|
||||
|
||||
all_destinations = bind.execute(
|
||||
sa.select(
|
||||
destinations_t.c.id,
|
||||
destinations_t.c.user_id,
|
||||
destinations_t.c.type,
|
||||
destinations_t.c.destination_value,
|
||||
).order_by(destinations_t.c.user_id, destinations_t.c.created_at)
|
||||
).fetchall()
|
||||
|
||||
companies_by_user: dict = {}
|
||||
for company_id, user_id in bind.execute(
|
||||
sa.select(companies_t.c.id, companies_t.c.user_id)
|
||||
).fetchall():
|
||||
companies_by_user.setdefault(user_id, []).append(company_id)
|
||||
|
||||
now = datetime.now(UTC)
|
||||
link_rows = [
|
||||
{"destination_id": dest_id, "company_id": company_id, "created_at": now, "updated_at": now}
|
||||
for dest_id, user_id, _type, _value in all_destinations
|
||||
for company_id in companies_by_user.get(user_id, [])
|
||||
]
|
||||
if link_rows:
|
||||
bind.execute(sa.insert(links_t), link_rows)
|
||||
|
||||
seen: dict[tuple, object] = {}
|
||||
duplicate_ids: list = []
|
||||
for dest_id, user_id, dtype, value in all_destinations:
|
||||
normalized_value = value.strip().lower() if dtype == "email" else value.strip()
|
||||
key = (user_id, dtype, normalized_value)
|
||||
if key in seen:
|
||||
duplicate_ids.append(dest_id)
|
||||
else:
|
||||
seen[key] = dest_id
|
||||
|
||||
if duplicate_ids:
|
||||
bind.execute(sa.delete(destinations_t).where(destinations_t.c.id.in_(duplicate_ids)))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("notification_destination_companies")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,47 @@
|
||||
"""add user_api_keys table
|
||||
|
||||
Revision ID: 6d296b533f7c
|
||||
Revises: 7e0adee63b3b
|
||||
Create Date: 2026-08-05 10:40:15.989218
|
||||
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = '6d296b533f7c'
|
||||
down_revision: str | None = '7e0adee63b3b'
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'user_api_keys',
|
||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
'provider',
|
||||
sa.Enum(
|
||||
'ANTHROPIC', 'BRAVE_SEARCH', 'NINJAPEAR', 'USPTO',
|
||||
name='apikeyprovider', native_enum=False, length=16,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column('encrypted_key', sa.Text(), nullable=False),
|
||||
sa.Column('id', sa.Uuid(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'provider', name='uq_user_api_keys_user_provider'),
|
||||
)
|
||||
op.create_index(op.f('ix_user_api_keys_user_id'), 'user_api_keys', ['user_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_user_api_keys_user_id'), table_name='user_api_keys')
|
||||
op.drop_table('user_api_keys')
|
||||
@@ -0,0 +1,64 @@
|
||||
"""create company enrichments table
|
||||
|
||||
Revision ID: 79e3aa041131
|
||||
Revises: 06e7f03cea36
|
||||
Create Date: 2026-08-03 18:43:27.311271
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "79e3aa041131"
|
||||
down_revision: str | None = "06e7f03cea36"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"company_enrichments",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"PENDING",
|
||||
"PARTIAL",
|
||||
"COMPLETE",
|
||||
"FAILED",
|
||||
name="enrichmentstatus",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("data", sa.JSON(), nullable=False),
|
||||
sa.Column("errors", sa.JSON(), nullable=False),
|
||||
sa.Column("credits_spent", sa.Integer(), nullable=True),
|
||||
sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_company_enrichments_company_id"),
|
||||
"company_enrichments",
|
||||
["company_id"],
|
||||
unique=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_company_enrichments_company_id"), table_name="company_enrichments")
|
||||
op.drop_table("company_enrichments")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,45 @@
|
||||
"""add password_history_entries table
|
||||
|
||||
Revision ID: 7e0adee63b3b
|
||||
Revises: ef56f181dbb9
|
||||
Create Date: 2026-08-05 07:24:33.677085
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "7e0adee63b3b"
|
||||
down_revision: str | None = "ef56f181dbb9"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"password_history_entries",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_password_history_entries_user_id"),
|
||||
"password_history_entries",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(
|
||||
op.f("ix_password_history_entries_user_id"), table_name="password_history_entries"
|
||||
)
|
||||
op.drop_table("password_history_entries")
|
||||
@@ -0,0 +1,186 @@
|
||||
"""create companies monitoring and notification tables
|
||||
|
||||
Revision ID: 8880af3b8aa1
|
||||
Revises: 05a3ccfe49b9
|
||||
Create Date: 2026-07-31 18:29:28.820437
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "8880af3b8aa1"
|
||||
down_revision: str | None = "05a3ccfe49b9"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"companies",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column("slug", sa.String(length=220), nullable=False),
|
||||
sa.Column("official_website", sa.String(length=500), nullable=True),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("monitoring_focus", sa.Text(), nullable=True),
|
||||
sa.Column("industry", sa.String(length=120), nullable=True),
|
||||
sa.Column("country", sa.String(length=120), nullable=True),
|
||||
sa.Column("region", sa.String(length=120), nullable=True),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum("ACTIVE", "PAUSED", name="companystatus", native_enum=False, length=20),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_companies_slug"), "companies", ["slug"], unique=False)
|
||||
op.create_index(op.f("ix_companies_user_id"), "companies", ["user_id"], unique=False)
|
||||
op.create_table(
|
||||
"notification_destinations",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"type",
|
||||
sa.Enum(
|
||||
"EMAIL", "SMS", "CONSOLE", name="notificationtype", native_enum=False, length=20
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("destination_value", sa.String(length=320), nullable=False),
|
||||
sa.Column("verified", sa.Boolean(), nullable=False),
|
||||
sa.Column("enabled", sa.Boolean(), nullable=False),
|
||||
sa.Column(
|
||||
"minimum_severity",
|
||||
sa.Enum(
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM",
|
||||
"LOW",
|
||||
name="severitylevel",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_notification_destinations_user_id"),
|
||||
"notification_destinations",
|
||||
["user_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_table(
|
||||
"company_aliases",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("alias", sa.String(length=200), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_company_aliases_company_id"), "company_aliases", ["company_id"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"competitors",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("name", sa.String(length=200), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_competitors_company_id"), "competitors", ["company_id"], unique=False)
|
||||
op.create_table(
|
||||
"monitor_configurations",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"frequency_type",
|
||||
sa.Enum(
|
||||
"HOURLY",
|
||||
"EVERY_6_HOURS",
|
||||
"EVERY_12_HOURS",
|
||||
"DAILY",
|
||||
"EVERY_2_DAYS",
|
||||
"WEEKLY",
|
||||
"EVERY_2_WEEKS",
|
||||
"MONTHLY",
|
||||
"CUSTOM",
|
||||
name="monitoringfrequency",
|
||||
native_enum=False,
|
||||
length=30,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("interval_minutes", sa.Integer(), nullable=True),
|
||||
sa.Column("cron_expression", sa.String(length=120), nullable=True),
|
||||
sa.Column("timezone", sa.String(length=64), nullable=False),
|
||||
sa.Column("enabled", sa.Boolean(), nullable=False),
|
||||
sa.Column("next_run", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("last_run", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"severity_threshold",
|
||||
sa.Enum(
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM",
|
||||
"LOW",
|
||||
name="severitylevel",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source_configuration", sa.JSON(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_monitor_configurations_company_id"),
|
||||
"monitor_configurations",
|
||||
["company_id"],
|
||||
unique=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_monitor_configurations_company_id"), table_name="monitor_configurations")
|
||||
op.drop_table("monitor_configurations")
|
||||
op.drop_index(op.f("ix_competitors_company_id"), table_name="competitors")
|
||||
op.drop_table("competitors")
|
||||
op.drop_index(op.f("ix_company_aliases_company_id"), table_name="company_aliases")
|
||||
op.drop_table("company_aliases")
|
||||
op.drop_index(
|
||||
op.f("ix_notification_destinations_user_id"), table_name="notification_destinations"
|
||||
)
|
||||
op.drop_table("notification_destinations")
|
||||
op.drop_index(op.f("ix_companies_user_id"), table_name="companies")
|
||||
op.drop_index(op.f("ix_companies_slug"), table_name="companies")
|
||||
op.drop_table("companies")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,96 @@
|
||||
"""create monitoring runs
|
||||
|
||||
Revision ID: 8ceefbd0a6a5
|
||||
Revises: 484419ccd357
|
||||
Create Date: 2026-07-31 19:10:32.580895
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "8ceefbd0a6a5"
|
||||
down_revision: str | None = "484419ccd357"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"monitoring_runs",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"trigger_type",
|
||||
sa.Enum(
|
||||
"SCHEDULED",
|
||||
"MANUAL",
|
||||
"INITIAL",
|
||||
"RETRY",
|
||||
name="monitoringruntrigger",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"QUEUED",
|
||||
"RUNNING",
|
||||
"SUCCESSFUL",
|
||||
"PARTIAL",
|
||||
"FAILED",
|
||||
name="monitoringrunstatus",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("sources_attempted", sa.Integer(), nullable=False),
|
||||
sa.Column("sources_successful", sa.Integer(), nullable=False),
|
||||
sa.Column("sources_failed", sa.Integer(), nullable=False),
|
||||
sa.Column("items_collected", sa.Integer(), nullable=False),
|
||||
sa.Column("changes_detected", sa.Integer(), nullable=False),
|
||||
sa.Column("error_summary", sa.Text(), nullable=True),
|
||||
sa.Column("worker_task_id", sa.String(length=255), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_monitoring_runs_company_id"), "monitoring_runs", ["company_id"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# snapshots.monitoring_run_id was added in the previous migration without
|
||||
# a FK constraint, since monitoring_runs didn't exist yet - add it now
|
||||
# that the table this phase creates exists. batch_alter_table so this
|
||||
# also works on SQLite (which can't ALTER TABLE ADD CONSTRAINT directly).
|
||||
with op.batch_alter_table("snapshots") as batch_op:
|
||||
batch_op.create_foreign_key(
|
||||
"fk_snapshots_monitoring_run_id",
|
||||
"monitoring_runs",
|
||||
["monitoring_run_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("snapshots") as batch_op:
|
||||
batch_op.drop_constraint("fk_snapshots_monitoring_run_id", type_="foreignkey")
|
||||
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_monitoring_runs_company_id"), table_name="monitoring_runs")
|
||||
op.drop_table("monitoring_runs")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,46 @@
|
||||
"""add system secrets table
|
||||
|
||||
Revision ID: 8db5bd1c31f8
|
||||
Revises: 6d296b533f7c
|
||||
Create Date: 2026-08-05 07:48:50.750288
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "8db5bd1c31f8"
|
||||
down_revision: str | None = "6d296b533f7c"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"system_secrets",
|
||||
sa.Column(
|
||||
"key",
|
||||
sa.Enum(
|
||||
"TURNSTILE_SITE_KEY",
|
||||
"TURNSTILE_SECRET",
|
||||
name="systemsecretkey",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("encrypted_value", sa.Text(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("key"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("system_secrets")
|
||||
@@ -0,0 +1,121 @@
|
||||
"""create alerts and notification deliveries
|
||||
|
||||
Revision ID: b5e0d93c89ac
|
||||
Revises: c06c845cdd10
|
||||
Create Date: 2026-07-31 22:15:07.310833
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "b5e0d93c89ac"
|
||||
down_revision: str | None = "c06c845cdd10"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"alerts",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("detected_change_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("title", sa.String(length=200), nullable=False),
|
||||
sa.Column("summary", sa.Text(), nullable=False),
|
||||
sa.Column("why_it_matters", sa.Text(), nullable=False),
|
||||
sa.Column(
|
||||
"severity",
|
||||
sa.Enum(
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM",
|
||||
"LOW",
|
||||
name="severitylevel",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("confidence", sa.Float(), nullable=False),
|
||||
sa.Column("read", sa.Boolean(), nullable=False),
|
||||
sa.Column("resolved", sa.Boolean(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["detected_change_id"], ["detected_changes.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_alerts_company_id"), "alerts", ["company_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_alerts_detected_change_id"), "alerts", ["detected_change_id"], unique=False
|
||||
)
|
||||
op.create_index(op.f("ix_alerts_user_id"), "alerts", ["user_id"], unique=False)
|
||||
op.create_table(
|
||||
"notification_deliveries",
|
||||
sa.Column("alert_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("destination_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("provider", sa.String(length=50), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"PENDING",
|
||||
"SENT",
|
||||
"FAILED",
|
||||
name="notificationdeliverystatus",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("attempt_count", sa.Integer(), nullable=False),
|
||||
sa.Column("last_attempt", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("external_message_id", sa.String(length=255), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["alert_id"], ["alerts.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["destination_id"], ["notification_destinations.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_notification_deliveries_alert_id"),
|
||||
"notification_deliveries",
|
||||
["alert_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_notification_deliveries_destination_id"),
|
||||
"notification_deliveries",
|
||||
["destination_id"],
|
||||
unique=False,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(
|
||||
op.f("ix_notification_deliveries_destination_id"), table_name="notification_deliveries"
|
||||
)
|
||||
op.drop_index(op.f("ix_notification_deliveries_alert_id"), table_name="notification_deliveries")
|
||||
op.drop_table("notification_deliveries")
|
||||
op.drop_index(op.f("ix_alerts_user_id"), table_name="alerts")
|
||||
op.drop_index(op.f("ix_alerts_detected_change_id"), table_name="alerts")
|
||||
op.drop_index(op.f("ix_alerts_company_id"), table_name="alerts")
|
||||
op.drop_table("alerts")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,68 @@
|
||||
"""create reports
|
||||
|
||||
Revision ID: c06c845cdd10
|
||||
Revises: cb61ba9570fb
|
||||
Create Date: 2026-07-31 19:52:22.947563
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c06c845cdd10"
|
||||
down_revision: str | None = "cb61ba9570fb"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"reports",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("monitoring_run_id", sa.Uuid(), nullable=True),
|
||||
sa.Column(
|
||||
"report_type",
|
||||
sa.Enum(
|
||||
"BASELINE",
|
||||
"UPDATE",
|
||||
"MONTHLY",
|
||||
"MANUAL",
|
||||
name="reporttype",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("title", sa.String(length=300), nullable=False),
|
||||
sa.Column("executive_summary", sa.Text(), nullable=False),
|
||||
sa.Column("structured_report", sa.JSON(), nullable=False),
|
||||
sa.Column("markdown_content", sa.Text(), nullable=False),
|
||||
sa.Column("model_provider", sa.String(length=50), nullable=False),
|
||||
sa.Column("model_name", sa.String(length=100), nullable=False),
|
||||
sa.Column("prompt_version", sa.String(length=20), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["monitoring_run_id"], ["monitoring_runs.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_reports_company_id"), "reports", ["company_id"], unique=False)
|
||||
op.create_index(
|
||||
op.f("ix_reports_monitoring_run_id"), "reports", ["monitoring_run_id"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_reports_monitoring_run_id"), table_name="reports")
|
||||
op.drop_index(op.f("ix_reports_company_id"), table_name="reports")
|
||||
op.drop_table("reports")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,41 @@
|
||||
"""add user known ips table
|
||||
|
||||
Revision ID: c34c769afc07
|
||||
Revises: 8db5bd1c31f8
|
||||
Create Date: 2026-08-05 09:13:20.293276
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "c34c769afc07"
|
||||
down_revision: str | None = "8db5bd1c31f8"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"user_known_ips",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("ip_address", sa.String(length=45), nullable=False),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "ip_address", name="uq_user_known_ips_user_ip"),
|
||||
)
|
||||
op.create_index(op.f("ix_user_known_ips_user_id"), "user_known_ips", ["user_id"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_user_known_ips_user_id"), table_name="user_known_ips")
|
||||
op.drop_table("user_known_ips")
|
||||
@@ -0,0 +1,107 @@
|
||||
"""create detected changes
|
||||
|
||||
Revision ID: cb61ba9570fb
|
||||
Revises: 8ceefbd0a6a5
|
||||
Create Date: 2026-07-31 19:34:49.775190
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "cb61ba9570fb"
|
||||
down_revision: str | None = "8ceefbd0a6a5"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"detected_changes",
|
||||
sa.Column("company_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("source_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("monitoring_run_id", sa.Uuid(), nullable=False),
|
||||
sa.Column("previous_snapshot_id", sa.Uuid(), nullable=True),
|
||||
sa.Column("current_snapshot_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"change_type",
|
||||
sa.Enum(
|
||||
"NEW_DOCUMENT",
|
||||
"REMOVED_DOCUMENT",
|
||||
"CONTENT_MODIFIED",
|
||||
"PRICE_CHANGE",
|
||||
"LEADERSHIP_CHANGE",
|
||||
"FILING_NEW",
|
||||
name="changetype",
|
||||
native_enum=False,
|
||||
length=30,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("raw_diff", sa.JSON(), nullable=False),
|
||||
sa.Column("significance_score", sa.Float(), nullable=False),
|
||||
sa.Column("confidence_score", sa.Float(), nullable=False),
|
||||
sa.Column(
|
||||
"severity",
|
||||
sa.Enum(
|
||||
"CRITICAL",
|
||||
"HIGH",
|
||||
"MEDIUM",
|
||||
"LOW",
|
||||
name="severitylevel",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.Enum(
|
||||
"NEW",
|
||||
"ACKNOWLEDGED",
|
||||
"DISMISSED",
|
||||
name="changestatus",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("summary", sa.String(length=500), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["company_id"], ["companies.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["current_snapshot_id"], ["snapshots.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["monitoring_run_id"], ["monitoring_runs.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["previous_snapshot_id"], ["snapshots.id"], ondelete="SET NULL"),
|
||||
sa.ForeignKeyConstraint(["source_id"], ["sources.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_detected_changes_company_id"), "detected_changes", ["company_id"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_detected_changes_monitoring_run_id"),
|
||||
"detected_changes",
|
||||
["monitoring_run_id"],
|
||||
unique=False,
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_detected_changes_source_id"), "detected_changes", ["source_id"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_detected_changes_source_id"), table_name="detected_changes")
|
||||
op.drop_index(op.f("ix_detected_changes_monitoring_run_id"), table_name="detected_changes")
|
||||
op.drop_index(op.f("ix_detected_changes_company_id"), table_name="detected_changes")
|
||||
op.drop_table("detected_changes")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,159 @@
|
||||
"""add email verification, ip throttle/ban, unban requests, user security events
|
||||
|
||||
Revision ID: ef56f181dbb9
|
||||
Revises: 79e3aa041131
|
||||
Create Date: 2026-08-05 00:46:28.371645
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "ef56f181dbb9"
|
||||
down_revision: str | None = "79e3aa041131"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"ip_bans",
|
||||
sa.Column("ip_address", sa.String(length=45), nullable=False),
|
||||
sa.Column("banned_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("reason", sa.String(length=255), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_ip_bans_ip_address"), "ip_bans", ["ip_address"], unique=True)
|
||||
op.create_table(
|
||||
"ip_throttle_state",
|
||||
sa.Column("ip_address", sa.String(length=45), nullable=False),
|
||||
sa.Column(
|
||||
"action",
|
||||
sa.Enum(
|
||||
"RESEND_VERIFICATION",
|
||||
"RESEND_RESET",
|
||||
"FAILED_LOGIN",
|
||||
name="throttleaction",
|
||||
native_enum=False,
|
||||
length=24,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("attempt_count", sa.Integer(), nullable=False),
|
||||
sa.Column("next_allowed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("timeout_until", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("offense_count", sa.Integer(), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
sa.UniqueConstraint("ip_address", "action", name="uq_ip_throttle_state_ip_action"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_ip_throttle_state_ip_address"), "ip_throttle_state", ["ip_address"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"unban_requests",
|
||||
sa.Column("ip_address", sa.String(length=45), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_unban_requests_ip_address"), "unban_requests", ["ip_address"], unique=False
|
||||
)
|
||||
op.create_table(
|
||||
"email_codes",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"purpose",
|
||||
sa.Enum(
|
||||
"VERIFY_EMAIL",
|
||||
"PASSWORD_RESET",
|
||||
name="emailcodepurpose",
|
||||
native_enum=False,
|
||||
length=20,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("code_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_email_codes_code_hash"), "email_codes", ["code_hash"], unique=False)
|
||||
op.create_table(
|
||||
"user_security_events",
|
||||
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||
sa.Column(
|
||||
"event_type",
|
||||
sa.Enum(
|
||||
"LOGIN_SUCCESS",
|
||||
"LOGIN_FAILED",
|
||||
"ACCOUNT_LOCKED",
|
||||
"PASSWORD_RESET_REQUESTED",
|
||||
"PASSWORD_RESET_COMPLETED",
|
||||
"EMAIL_VERIFICATION_SENT",
|
||||
"EMAIL_VERIFIED",
|
||||
name="securityeventtype",
|
||||
native_enum=False,
|
||||
length=32,
|
||||
),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("ip_address", sa.String(length=45), nullable=False),
|
||||
sa.Column("id", sa.Uuid(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_user_security_events_user_id"), "user_security_events", ["user_id"], unique=False
|
||||
)
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("email_verified", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
)
|
||||
op.add_column(
|
||||
"users", sa.Column("failed_login_count", sa.Integer(), nullable=False, server_default="0")
|
||||
)
|
||||
op.add_column("users", sa.Column("locked_at", sa.DateTime(timezone=True), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("users", "locked_at")
|
||||
op.drop_column("users", "failed_login_count")
|
||||
op.drop_column("users", "email_verified")
|
||||
op.drop_index(op.f("ix_user_security_events_user_id"), table_name="user_security_events")
|
||||
op.drop_table("user_security_events")
|
||||
op.drop_index(op.f("ix_email_codes_code_hash"), table_name="email_codes")
|
||||
op.drop_table("email_codes")
|
||||
op.drop_index(op.f("ix_unban_requests_ip_address"), table_name="unban_requests")
|
||||
op.drop_table("unban_requests")
|
||||
op.drop_index(op.f("ix_ip_throttle_state_ip_address"), table_name="ip_throttle_state")
|
||||
op.drop_table("ip_throttle_state")
|
||||
op.drop_index(op.f("ix_ip_bans_ip_address"), table_name="ip_bans")
|
||||
op.drop_table("ip_bans")
|
||||
# ### end Alembic commands ###
|
||||
@@ -0,0 +1,39 @@
|
||||
"""add company headquarters and public_identifiers
|
||||
|
||||
Revision ID: f01919a99ee9
|
||||
Revises: b5e0d93c89ac
|
||||
Create Date: 2026-08-01 15:28:37.144461
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "f01919a99ee9"
|
||||
down_revision: str | None = "b5e0d93c89ac"
|
||||
branch_labels: Sequence[str] | str | None = None
|
||||
depends_on: Sequence[str] | str | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("companies", sa.Column("headquarters", sa.String(length=200), nullable=True))
|
||||
# server_default backfills existing rows; NOT NULL alone would fail
|
||||
# against a non-empty companies table (Postgres rejects adding a NOT
|
||||
# NULL column with no default when rows already exist).
|
||||
op.add_column(
|
||||
"companies",
|
||||
sa.Column("public_identifiers", sa.JSON(), nullable=False, server_default="{}"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("companies", "public_identifiers")
|
||||
op.drop_column("companies", "headquarters")
|
||||
# ### end Alembic commands ###
|
||||
Reference in New Issue
Block a user