Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""initial schema
|
||||
|
||||
Revision ID: 20260711_0001
|
||||
Revises:
|
||||
Create Date: 2026-07-11 14:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260711_0001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("email", sa.String(length=255), nullable=False),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=False),
|
||||
sa.Column("role", sa.String(length=16), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("failed_login_attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("locked_until", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("email_verified_at", sa.DateTime(timezone=True), nullable=True),
|
||||
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("email"),
|
||||
)
|
||||
op.create_index("idx_users_email", "users", ["email"], unique=True)
|
||||
op.create_index("idx_users_status", "users", ["status"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"user_profiles",
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("display_name", sa.String(length=120), nullable=False),
|
||||
sa.Column("avatar_url", sa.String(length=512), nullable=True),
|
||||
sa.Column("metadata_json", sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("user_id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"refresh_tokens",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("family_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token_hash"),
|
||||
)
|
||||
op.create_index("idx_refresh_tokens_user", "refresh_tokens", ["user_id"], unique=False)
|
||||
op.create_index("idx_refresh_tokens_family", "refresh_tokens", ["family_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"password_reset_tokens",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("token_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.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token_hash"),
|
||||
)
|
||||
op.create_index("idx_password_reset_user", "password_reset_tokens", ["user_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"email_verification_tokens",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("token_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.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token_hash"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"content_pages",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("slug", sa.String(length=120), nullable=False),
|
||||
sa.Column("title", sa.String(length=255), nullable=False),
|
||||
sa.Column("body", sa.Text(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("author_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["author_id"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("slug"),
|
||||
)
|
||||
op.create_index("idx_content_slug", "content_pages", ["slug"], unique=True)
|
||||
op.create_index("idx_content_status", "content_pages", ["status"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_content_status", table_name="content_pages")
|
||||
op.drop_index("idx_content_slug", table_name="content_pages")
|
||||
op.drop_table("content_pages")
|
||||
op.drop_table("email_verification_tokens")
|
||||
op.drop_index("idx_password_reset_user", table_name="password_reset_tokens")
|
||||
op.drop_table("password_reset_tokens")
|
||||
op.drop_index("idx_refresh_tokens_family", table_name="refresh_tokens")
|
||||
op.drop_index("idx_refresh_tokens_user", table_name="refresh_tokens")
|
||||
op.drop_table("refresh_tokens")
|
||||
op.drop_table("user_profiles")
|
||||
op.drop_index("idx_users_status", table_name="users")
|
||||
op.drop_index("idx_users_email", table_name="users")
|
||||
op.drop_table("users")
|
||||
@@ -0,0 +1,33 @@
|
||||
"""seed admin and demo content
|
||||
|
||||
Revision ID: 20260711_0002
|
||||
Revises: 20260711_0001
|
||||
Create Date: 2026-07-11 14:05:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260711_0002"
|
||||
down_revision: Union[str, None] = "20260711_0001"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Demo users and CMS pages are seeded on API startup (app.main lifespan → run_seed)
|
||||
# after all schema migrations, including is_superuser (20260714_0003).
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
from sqlalchemy import delete
|
||||
|
||||
from app.core.database import session_scope
|
||||
from app.modules.content.models import ContentPage
|
||||
from app.modules.users.models import User
|
||||
|
||||
with session_scope() as db:
|
||||
db.execute(delete(ContentPage).where(ContentPage.slug.in_(["about", "privacy", "terms"])))
|
||||
db.execute(delete(User).where(User.email == "admin@compton.example"))
|
||||
@@ -0,0 +1,36 @@
|
||||
"""add is_superuser to users
|
||||
|
||||
Revision ID: 20260714_0003
|
||||
Revises: 20260711_0002
|
||||
Create Date: 2026-07-14 12:45:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260714_0003"
|
||||
down_revision: Union[str, None] = "20260711_0002"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"users",
|
||||
sa.Column("is_superuser", sa.Boolean(), nullable=False, server_default=sa.false()),
|
||||
)
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != "sqlite":
|
||||
op.alter_column("users", "is_superuser", server_default=None)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE users SET is_superuser = true "
|
||||
"WHERE email = 'admin@compton.example' AND role = 'admin'"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("users", "is_superuser")
|
||||
@@ -0,0 +1,55 @@
|
||||
"""db security constraints and token indexes
|
||||
|
||||
Revision ID: 20260714_0004
|
||||
Revises: 20260714_0003
|
||||
Create Date: 2026-07-14 13:30:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260714_0004"
|
||||
down_revision: Union[str, None] = "20260714_0003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != "sqlite":
|
||||
op.create_check_constraint(
|
||||
"ck_users_role_allowed",
|
||||
"users",
|
||||
"role IN ('user', 'admin')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_users_status_allowed",
|
||||
"users",
|
||||
"status IN ('pending', 'active', 'blocked')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_users_superuser_requires_admin",
|
||||
"users",
|
||||
"(is_superuser = false) OR (role = 'admin')",
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_content_pages_status_allowed",
|
||||
"content_pages",
|
||||
"status IN ('draft', 'published')",
|
||||
)
|
||||
op.create_index("idx_refresh_tokens_expires_at", "refresh_tokens", ["expires_at"], unique=False)
|
||||
op.create_index("idx_password_reset_expires_at", "password_reset_tokens", ["expires_at"], unique=False)
|
||||
op.create_index("idx_email_verify_expires_at", "email_verification_tokens", ["expires_at"], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("idx_email_verify_expires_at", table_name="email_verification_tokens")
|
||||
op.drop_index("idx_password_reset_expires_at", table_name="password_reset_tokens")
|
||||
op.drop_index("idx_refresh_tokens_expires_at", table_name="refresh_tokens")
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name != "sqlite":
|
||||
op.drop_constraint("ck_content_pages_status_allowed", "content_pages", type_="check")
|
||||
op.drop_constraint("ck_users_superuser_requires_admin", "users", type_="check")
|
||||
op.drop_constraint("ck_users_status_allowed", "users", type_="check")
|
||||
op.drop_constraint("ck_users_role_allowed", "users", type_="check")
|
||||
@@ -0,0 +1,35 @@
|
||||
"""backfill superuser flag for seeded admin
|
||||
|
||||
Revision ID: 20260714_0005
|
||||
Revises: 20260714_0004
|
||||
Create Date: 2026-07-14 14:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260714_0005"
|
||||
down_revision: Union[str, None] = "20260714_0004"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE users SET is_superuser = true "
|
||||
"WHERE email = 'admin@compton.example' AND role = 'admin'"
|
||||
)
|
||||
)
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE users SET is_superuser = false "
|
||||
"WHERE email IN ('ops@compton.example', 'user@compton.example')"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,367 @@
|
||||
"""enterprise tenant and sync durability schema
|
||||
|
||||
Revision ID: 20260715_0006
|
||||
Revises: 20260714_0005
|
||||
Create Date: 2026-07-15 12:00:00
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260715_0006"
|
||||
down_revision: Union[str, None] = "20260714_0005"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"enterprises",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("slug", sa.String(length=64), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), 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("slug"),
|
||||
)
|
||||
op.create_index("idx_enterprises_slug", "enterprises", ["slug"], unique=True)
|
||||
op.create_index("idx_enterprises_status", "enterprises", ["status"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"enterprise_members",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("role", sa.String(length=32), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "enterprise_id", name="uq_enterprise_members_user_enterprise"),
|
||||
)
|
||||
op.create_index("idx_enterprise_members_user", "enterprise_members", ["user_id"], unique=False)
|
||||
op.create_index("idx_enterprise_members_enterprise", "enterprise_members", ["enterprise_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"farm_hubs",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=False),
|
||||
sa.Column("hub_site_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("url", sa.String(length=512), nullable=True),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("last_seen", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("wesp_version", sa.String(length=64), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("hub_site_id"),
|
||||
)
|
||||
op.create_index("idx_farm_hubs_enterprise", "farm_hubs", ["enterprise_id"], unique=False)
|
||||
op.create_index("idx_farm_hubs_status", "farm_hubs", ["status"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"user_farm_access",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("user_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "farm_hub_id", name="uq_user_farm_access_user_farm"),
|
||||
)
|
||||
op.create_index("idx_user_farm_access_user", "user_farm_access", ["user_id"], unique=False)
|
||||
op.create_index("idx_user_farm_access_farm", "user_farm_access", ["farm_hub_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"hub_credentials",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("secret_hash", sa.String(length=255), nullable=False),
|
||||
sa.Column("paired_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_hub_credentials_farm", "hub_credentials", ["farm_hub_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"hub_pairing_sessions",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("code", sa.String(length=6), nullable=False),
|
||||
sa.Column("code_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("confirmed_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_hub_pairing_code", "hub_pairing_sessions", ["code"], unique=False)
|
||||
op.create_index("idx_hub_pairing_enterprise", "hub_pairing_sessions", ["enterprise_id"], unique=False)
|
||||
|
||||
_create_sync_tables()
|
||||
_create_zootech_tables()
|
||||
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name == "postgresql":
|
||||
_create_rls_policies()
|
||||
|
||||
|
||||
def _create_sync_tables() -> None:
|
||||
op.create_table(
|
||||
"sync_outbox",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("event_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("origin", sa.String(length=32), nullable=False),
|
||||
sa.Column("origin_site_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("seq", sa.BigInteger(), nullable=False),
|
||||
sa.Column("domain", sa.String(length=16), nullable=False),
|
||||
sa.Column("table_name", sa.String(length=64), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("action", sa.String(length=16), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("emitted_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("event_id"),
|
||||
)
|
||||
op.create_index("idx_sync_outbox_enterprise_status", "sync_outbox", ["enterprise_id", "status"], unique=False)
|
||||
op.create_index("idx_sync_outbox_table_record", "sync_outbox", ["table_name", "record_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"sync_event_log",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("event_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("origin", sa.String(length=32), nullable=False),
|
||||
sa.Column("origin_site_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("seq", sa.BigInteger(), nullable=False),
|
||||
sa.Column("domain", sa.String(length=16), nullable=False),
|
||||
sa.Column("table_name", sa.String(length=64), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("action", sa.String(length=16), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=False),
|
||||
sa.Column("received_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("event_id"),
|
||||
)
|
||||
op.create_index("idx_sync_event_log_enterprise", "sync_event_log", ["enterprise_id"], unique=False)
|
||||
op.create_index("idx_sync_event_log_seq", "sync_event_log", ["seq"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"sync_applied_events",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("site_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("event_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("applied_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("site_id", "event_id", name="uq_sync_applied_events_site_event"),
|
||||
)
|
||||
op.create_index("idx_sync_applied_enterprise", "sync_applied_events", ["enterprise_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"sync_cursors",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("direction", sa.String(length=16), nullable=False),
|
||||
sa.Column("last_acked_seq", sa.BigInteger(), nullable=False),
|
||||
sa.Column("last_pulled_seq", sa.BigInteger(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("farm_hub_id", "direction", name="uq_sync_cursors_hub_direction"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sync_record_state",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("table_name", sa.String(length=64), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("agreed_version", sa.Integer(), nullable=False),
|
||||
sa.Column("agreed_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("last_event_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("enterprise_id", "table_name", "record_id", name="uq_sync_record_state_ent_table_record"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sync_conflicts",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("table_name", sa.String(length=64), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("orchestrator_snapshot_json", sa.Text(), nullable=False),
|
||||
sa.Column("hub_snapshot_json", sa.Text(), nullable=False),
|
||||
sa.Column("held_event_ids_json", sa.Text(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("resolution", sa.String(length=32), nullable=True),
|
||||
sa.Column("resolved_by", sa.String(length=36), nullable=True),
|
||||
sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="SET NULL"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("idx_sync_conflicts_status", "sync_conflicts", ["status"], unique=False)
|
||||
op.create_index("idx_sync_conflicts_record", "sync_conflicts", ["record_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"sync_reconcile_runs",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("mismatches_count", sa.Integer(), nullable=False),
|
||||
sa.Column("auto_repaired", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"report_sync_state",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("last_report_cursor", sa.BigInteger(), nullable=False),
|
||||
sa.Column("last_pull_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["enterprise_id"], ["enterprises.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["farm_hub_id"], ["farm_hubs.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("farm_hub_id"),
|
||||
)
|
||||
|
||||
|
||||
def _create_zootech_tables() -> None:
|
||||
op.create_table(
|
||||
"zootech_component",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("type", sa.String(length=100), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=False),
|
||||
sa.Column("dry_matter", sa.Float(), nullable=False),
|
||||
sa.Column("protein", sa.Float(), nullable=False),
|
||||
sa.Column("energy", sa.Float(), nullable=False),
|
||||
sa.Column("price", sa.Float(), nullable=False),
|
||||
sa.Column("external_no", sa.Integer(), nullable=True),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), 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("enterprise_id", "id", name="uq_zootech_component_ent_id"),
|
||||
)
|
||||
op.create_index("idx_zootech_component_enterprise", "zootech_component", ["enterprise_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"zootech_recipe",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("heads_per_trip", sa.Integer(), nullable=False),
|
||||
sa.Column("mixing_time", sa.Integer(), nullable=False),
|
||||
sa.Column("trip_percent", sa.Float(), nullable=False),
|
||||
sa.Column("dry_matter_locked", sa.Boolean(), nullable=False),
|
||||
sa.Column("unloading_link_broken", sa.Boolean(), nullable=False),
|
||||
sa.Column("target_component_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("ration_type", sa.String(length=10), nullable=True),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_recipe_ent_id"),
|
||||
)
|
||||
op.create_index("idx_zootech_recipe_enterprise", "zootech_recipe", ["enterprise_id"], unique=False)
|
||||
|
||||
op.create_table(
|
||||
"zootech_ingredient",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("recipe_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("component_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("amount", sa.Float(), nullable=False),
|
||||
sa.Column("weight_per_head", sa.Float(), nullable=True),
|
||||
sa.Column("dry_matter", sa.Float(), nullable=False),
|
||||
sa.Column("dry_matter_per_head", sa.Float(), nullable=True),
|
||||
sa.Column("order", sa.Integer(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), 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("enterprise_id", "id", name="uq_zootech_ingredient_ent_id"),
|
||||
)
|
||||
op.create_index("idx_zootech_ingredient_enterprise", "zootech_ingredient", ["enterprise_id"], unique=False)
|
||||
op.create_index("idx_zootech_ingredient_recipe", "zootech_ingredient", ["recipe_id"], unique=False)
|
||||
|
||||
|
||||
def _create_rls_policies() -> None:
|
||||
for table in ("zootech_component", "zootech_recipe", "zootech_ingredient"):
|
||||
op.execute(f"ALTER TABLE {table} ENABLE ROW LEVEL SECURITY")
|
||||
op.execute(
|
||||
f"""
|
||||
CREATE POLICY tenant_isolation_{table} ON {table}
|
||||
USING (enterprise_id = current_setting('app.enterprise_id', true))
|
||||
WITH CHECK (enterprise_id = current_setting('app.enterprise_id', true))
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name == "postgresql":
|
||||
for table in ("zootech_component", "zootech_recipe", "zootech_ingredient"):
|
||||
op.execute(f"DROP POLICY IF EXISTS tenant_isolation_{table} ON {table}")
|
||||
|
||||
for table in (
|
||||
"zootech_ingredient",
|
||||
"zootech_recipe",
|
||||
"zootech_component",
|
||||
"report_sync_state",
|
||||
"sync_reconcile_runs",
|
||||
"sync_conflicts",
|
||||
"sync_record_state",
|
||||
"sync_cursors",
|
||||
"sync_applied_events",
|
||||
"sync_event_log",
|
||||
"sync_outbox",
|
||||
"hub_pairing_sessions",
|
||||
"hub_credentials",
|
||||
"user_farm_access",
|
||||
"farm_hubs",
|
||||
"enterprise_members",
|
||||
"enterprises",
|
||||
):
|
||||
op.drop_table(table)
|
||||
@@ -0,0 +1,180 @@
|
||||
"""zootech full catalog tables
|
||||
|
||||
Revision ID: 20260716_0007
|
||||
Revises: 20260715_0006
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260716_0007"
|
||||
down_revision: Union[str, None] = "20260715_0006"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _simple_catalog(name: str, uq: str) -> None:
|
||||
op.create_table(
|
||||
name,
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name=uq),
|
||||
)
|
||||
op.create_index(f"idx_{name}_enterprise", name, ["enterprise_id"])
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"zootech_unloading_group",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("recipe_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_unloading_group_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feed_mixer",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_feed_mixer_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feed_dispenser",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_feed_dispenser_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feeding_location",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_feeding_location_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feeding_period",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("dispenser_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_feeding_period_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feeding_point",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("name", sa.String(length=100), nullable=False),
|
||||
sa.Column("period_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_feeding_point_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_period_recipes",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("period_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("recipe_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("order", sa.Integer(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("period_id", "recipe_id"),
|
||||
sa.UniqueConstraint("enterprise_id", "period_id", "recipe_id", name="uq_zootech_period_recipes_ent"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_trip",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("mixer_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("recipe_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("status", sa.String(length=20), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
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("enterprise_id", "id", name="uq_zootech_trip_ent_id"),
|
||||
)
|
||||
for tbl, uq in (
|
||||
("zootech_daily_trip_skip", "uq_zootech_daily_trip_skip_ent_id"),
|
||||
("zootech_daily_ingredient_skip", "uq_zootech_daily_ingredient_skip_ent_id"),
|
||||
("zootech_daily_unloading_group_skip", "uq_zootech_daily_unloading_group_skip_ent_id"),
|
||||
("zootech_daily_ingredient_replacement", "uq_zootech_daily_ingredient_replacement_ent_id"),
|
||||
("zootech_daily_component_norm_adjustment", "uq_zootech_daily_component_norm_adjustment_ent_id"),
|
||||
):
|
||||
_simple_catalog(tbl, uq)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for tbl in (
|
||||
"zootech_daily_component_norm_adjustment",
|
||||
"zootech_daily_ingredient_replacement",
|
||||
"zootech_daily_unloading_group_skip",
|
||||
"zootech_daily_ingredient_skip",
|
||||
"zootech_daily_trip_skip",
|
||||
"zootech_trip",
|
||||
"zootech_period_recipes",
|
||||
"zootech_feeding_point",
|
||||
"zootech_feeding_period",
|
||||
"zootech_feeding_location",
|
||||
"zootech_feed_dispenser",
|
||||
"zootech_feed_mixer",
|
||||
"zootech_unloading_group",
|
||||
):
|
||||
op.drop_table(tbl)
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Report tables + report outbox
|
||||
|
||||
Revision ID: 20260716_0008
|
||||
Revises: 20260716_0007
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260716_0008"
|
||||
down_revision: Union[str, None] = "20260716_0007"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _report_table(name: str) -> None:
|
||||
op.create_table(
|
||||
name,
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("trip_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("status", sa.String(length=32), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), 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("enterprise_id", "id", name=f"uq_{name}_ent_id"),
|
||||
)
|
||||
op.create_index(f"idx_{name}_enterprise", name, ["enterprise_id"])
|
||||
op.create_index(f"idx_{name}_farm_hub", name, ["farm_hub_id"])
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
_report_table("zootech_loading_report")
|
||||
_report_table("zootech_unloading_report")
|
||||
op.create_table(
|
||||
"zootech_feed_alert",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("alert_type", sa.String(length=64), nullable=False),
|
||||
sa.Column("severity", sa.String(length=16), nullable=False),
|
||||
sa.Column("message", sa.Text(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("is_deleted", sa.Boolean(), 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("enterprise_id", "id", name="uq_zootech_feed_alert_ent_id"),
|
||||
)
|
||||
op.create_table(
|
||||
"report_outbox",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("farm_hub_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("event_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("table_name", sa.String(length=64), nullable=False),
|
||||
sa.Column("record_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("action", sa.String(length=16), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=False),
|
||||
sa.Column("status", sa.String(length=16), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("event_id"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("report_outbox")
|
||||
op.drop_table("zootech_feed_alert")
|
||||
op.drop_table("zootech_unloading_report")
|
||||
op.drop_table("zootech_loading_report")
|
||||
@@ -0,0 +1,45 @@
|
||||
"""org settings + feed quality (enterprise scoped)
|
||||
|
||||
Revision ID: 20260716_0009
|
||||
Revises: 20260716_0008
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260716_0009"
|
||||
down_revision: Union[str, None] = "20260716_0008"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"zootech_org_settings",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("enterprise_id", name="uq_zootech_org_settings_enterprise"),
|
||||
)
|
||||
op.create_table(
|
||||
"zootech_feed_quality_settings",
|
||||
sa.Column("enterprise_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("payload_json", sa.Text(), nullable=False),
|
||||
sa.Column("version", sa.Integer(), nullable=False),
|
||||
sa.Column("content_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("enterprise_id", name="uq_zootech_feed_quality_settings_enterprise"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("zootech_feed_quality_settings")
|
||||
op.drop_table("zootech_org_settings")
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Widen sync record_id for composite keys (period_recipes uuid:uuid)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision = "20260716_0010"
|
||||
down_revision = "20260716_0009"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
_TABLES = ("sync_outbox", "sync_event_log", "sync_record_state", "sync_conflicts")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for table in _TABLES:
|
||||
op.alter_column(
|
||||
table,
|
||||
"record_id",
|
||||
existing_type=sa.String(length=64),
|
||||
type_=sa.String(length=128),
|
||||
existing_nullable=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
for table in reversed(_TABLES):
|
||||
op.alter_column(
|
||||
table,
|
||||
"record_id",
|
||||
existing_type=sa.String(length=128),
|
||||
type_=sa.String(length=64),
|
||||
existing_nullable=False,
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Add payload_json to zootech_feed_alert
|
||||
|
||||
Revision ID: 20260716_0011
|
||||
Revises: 20260716_0010
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260716_0011"
|
||||
down_revision: Union[str, None] = "20260716_0010"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("zootech_feed_alert", sa.Column("payload_json", sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("zootech_feed_alert", "payload_json")
|
||||
Reference in New Issue
Block a user