Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user