Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.core.crypto import hash_opaque_token
|
||||
from app.core.database import session_scope
|
||||
from app.modules.sync.models import (
|
||||
Enterprise,
|
||||
EnterpriseMember,
|
||||
FarmHub,
|
||||
HubCredential,
|
||||
HubPairingSession,
|
||||
SyncAppliedEvent,
|
||||
SyncConflict,
|
||||
SyncCursor,
|
||||
SyncEventLog,
|
||||
SyncOutbox,
|
||||
SyncRecordState,
|
||||
UserFarmAccess,
|
||||
)
|
||||
|
||||
|
||||
def _detach(db, instance):
|
||||
db.refresh(instance)
|
||||
db.expunge(instance)
|
||||
return instance
|
||||
|
||||
|
||||
def get_enterprise_by_id(enterprise_id: str) -> Enterprise | None:
|
||||
with session_scope() as db:
|
||||
row = db.get(Enterprise, enterprise_id)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def get_enterprise_by_slug(slug: str) -> Enterprise | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(select(Enterprise).where(Enterprise.slug == slug))
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def create_enterprise(name: str, slug: str) -> Enterprise:
|
||||
with session_scope() as db:
|
||||
row = Enterprise(name=name, slug=slug, status="active")
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def get_member(user_id: str, enterprise_id: str) -> EnterpriseMember | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(EnterpriseMember).where(
|
||||
EnterpriseMember.user_id == user_id,
|
||||
EnterpriseMember.enterprise_id == enterprise_id,
|
||||
)
|
||||
)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def add_member(user_id: str, enterprise_id: str, role: str) -> EnterpriseMember:
|
||||
with session_scope() as db:
|
||||
row = EnterpriseMember(user_id=user_id, enterprise_id=enterprise_id, role=role)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def list_members(enterprise_id: str) -> list[EnterpriseMember]:
|
||||
with session_scope() as db:
|
||||
rows = list(
|
||||
db.scalars(
|
||||
select(EnterpriseMember).where(EnterpriseMember.enterprise_id == enterprise_id)
|
||||
)
|
||||
)
|
||||
return [_detach(db, row) for row in rows]
|
||||
|
||||
|
||||
def set_member_role(user_id: str, enterprise_id: str, role: str) -> EnterpriseMember | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(EnterpriseMember).where(
|
||||
EnterpriseMember.user_id == user_id,
|
||||
EnterpriseMember.enterprise_id == enterprise_id,
|
||||
)
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
row.role = role
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def list_farm_access(user_id: str, enterprise_id: str) -> list[str]:
|
||||
with session_scope() as db:
|
||||
rows = db.scalars(
|
||||
select(UserFarmAccess.farm_hub_id)
|
||||
.join(FarmHub, FarmHub.id == UserFarmAccess.farm_hub_id)
|
||||
.where(UserFarmAccess.user_id == user_id, FarmHub.enterprise_id == enterprise_id)
|
||||
).all()
|
||||
return list(rows)
|
||||
|
||||
|
||||
def grant_farm_access(user_id: str, farm_hub_id: str) -> UserFarmAccess:
|
||||
with session_scope() as db:
|
||||
existing = db.scalar(
|
||||
select(UserFarmAccess).where(
|
||||
UserFarmAccess.user_id == user_id,
|
||||
UserFarmAccess.farm_hub_id == farm_hub_id,
|
||||
)
|
||||
)
|
||||
if existing:
|
||||
return _detach(db, existing)
|
||||
row = UserFarmAccess(user_id=user_id, farm_hub_id=farm_hub_id)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def get_farm_hub_by_site_id(hub_site_id: str) -> FarmHub | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(select(FarmHub).where(FarmHub.hub_site_id == hub_site_id))
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def get_farm_hub_by_id(farm_hub_id: str) -> FarmHub | None:
|
||||
with session_scope() as db:
|
||||
row = db.get(FarmHub, farm_hub_id)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def list_farm_hubs(enterprise_id: str) -> list[FarmHub]:
|
||||
with session_scope() as db:
|
||||
rows = list(db.scalars(select(FarmHub).where(FarmHub.enterprise_id == enterprise_id).order_by(FarmHub.name)))
|
||||
return [_detach(db, row) for row in rows]
|
||||
|
||||
|
||||
def create_pairing_session(enterprise_id: str, code: str, code_hash: str, ttl_minutes: int = 15) -> HubPairingSession:
|
||||
with session_scope() as db:
|
||||
row = HubPairingSession(
|
||||
enterprise_id=enterprise_id,
|
||||
code=code,
|
||||
code_hash=code_hash,
|
||||
expires_at=datetime.now(UTC) + timedelta(minutes=ttl_minutes),
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def get_pairing_session_by_code_hash(code_hash: str) -> HubPairingSession | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(HubPairingSession).where(
|
||||
HubPairingSession.code_hash == code_hash,
|
||||
HubPairingSession.confirmed_at.is_(None),
|
||||
)
|
||||
)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def confirm_pairing_session(session_id: str, farm_hub_id: str) -> None:
|
||||
with session_scope() as db:
|
||||
row = db.get(HubPairingSession, session_id)
|
||||
if row:
|
||||
row.confirmed_at = datetime.now(UTC)
|
||||
row.farm_hub_id = farm_hub_id
|
||||
|
||||
|
||||
def create_farm_hub(
|
||||
enterprise_id: str,
|
||||
name: str,
|
||||
hub_site_id: str,
|
||||
url: str | None = None,
|
||||
) -> FarmHub:
|
||||
with session_scope() as db:
|
||||
row = FarmHub(
|
||||
enterprise_id=enterprise_id,
|
||||
name=name,
|
||||
hub_site_id=hub_site_id,
|
||||
url=url,
|
||||
status="active",
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def create_hub_credential(farm_hub_id: str, api_key: str) -> HubCredential:
|
||||
from app.core.crypto import hash_opaque_token as _hash
|
||||
|
||||
with session_scope() as db:
|
||||
row = HubCredential(farm_hub_id=farm_hub_id, secret_hash=_hash(api_key))
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def verify_hub_credential(hub_site_id: str, api_key: str) -> FarmHub | None:
|
||||
from app.core.crypto import hash_opaque_token as _hash
|
||||
|
||||
key_hash = _hash(api_key)
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(FarmHub)
|
||||
.join(HubCredential, HubCredential.farm_hub_id == FarmHub.id)
|
||||
.where(
|
||||
FarmHub.hub_site_id == hub_site_id,
|
||||
HubCredential.secret_hash == key_hash,
|
||||
HubCredential.revoked_at.is_(None),
|
||||
)
|
||||
)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def update_hub_heartbeat(farm_hub_id: str, wesp_version: str | None) -> None:
|
||||
with session_scope() as db:
|
||||
row = db.get(FarmHub, farm_hub_id)
|
||||
if row:
|
||||
row.last_seen = datetime.now(UTC)
|
||||
if wesp_version:
|
||||
row.wesp_version = wesp_version
|
||||
|
||||
|
||||
def next_seq(enterprise_id: str) -> int:
|
||||
with session_scope() as db:
|
||||
current = db.scalar(
|
||||
select(func.max(SyncEventLog.seq)).where(SyncEventLog.enterprise_id == enterprise_id)
|
||||
)
|
||||
return int(current or 0) + 1
|
||||
|
||||
|
||||
def event_exists(event_id: str) -> bool:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(select(SyncEventLog.id).where(SyncEventLog.event_id == event_id))
|
||||
return row is not None
|
||||
|
||||
|
||||
def append_event_log(**kwargs) -> SyncEventLog:
|
||||
with session_scope() as db:
|
||||
row = SyncEventLog(**kwargs)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def applied_event_exists(site_id: str, event_id: str) -> bool:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(SyncAppliedEvent.id).where(
|
||||
SyncAppliedEvent.site_id == site_id,
|
||||
SyncAppliedEvent.event_id == event_id,
|
||||
)
|
||||
)
|
||||
return row is not None
|
||||
|
||||
|
||||
def mark_applied(enterprise_id: str, site_id: str, event_id: str) -> None:
|
||||
with session_scope() as db:
|
||||
db.add(
|
||||
SyncAppliedEvent(
|
||||
enterprise_id=enterprise_id,
|
||||
site_id=site_id,
|
||||
event_id=event_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_record_state(enterprise_id: str, table_name: str, record_id: str) -> SyncRecordState | None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(SyncRecordState).where(
|
||||
SyncRecordState.enterprise_id == enterprise_id,
|
||||
SyncRecordState.table_name == table_name,
|
||||
SyncRecordState.record_id == record_id,
|
||||
)
|
||||
)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def upsert_record_state(
|
||||
enterprise_id: str,
|
||||
table_name: str,
|
||||
record_id: str,
|
||||
agreed_version: int,
|
||||
agreed_hash: str,
|
||||
last_event_id: str,
|
||||
) -> None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(SyncRecordState).where(
|
||||
SyncRecordState.enterprise_id == enterprise_id,
|
||||
SyncRecordState.table_name == table_name,
|
||||
SyncRecordState.record_id == record_id,
|
||||
)
|
||||
)
|
||||
if row:
|
||||
row.agreed_version = agreed_version
|
||||
row.agreed_hash = agreed_hash
|
||||
row.last_event_id = last_event_id
|
||||
row.updated_at = datetime.now(UTC)
|
||||
else:
|
||||
db.add(
|
||||
SyncRecordState(
|
||||
enterprise_id=enterprise_id,
|
||||
table_name=table_name,
|
||||
record_id=record_id,
|
||||
agreed_version=agreed_version,
|
||||
agreed_hash=agreed_hash,
|
||||
last_event_id=last_event_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def list_conflicts(enterprise_id: str, status: str = "pending") -> list[SyncConflict]:
|
||||
with session_scope() as db:
|
||||
rows = list(
|
||||
db.scalars(
|
||||
select(SyncConflict)
|
||||
.where(SyncConflict.enterprise_id == enterprise_id, SyncConflict.status == status)
|
||||
.order_by(SyncConflict.created_at.desc())
|
||||
)
|
||||
)
|
||||
return [_detach(db, row) for row in rows]
|
||||
|
||||
|
||||
def get_conflict(conflict_id: str) -> SyncConflict | None:
|
||||
with session_scope() as db:
|
||||
row = db.get(SyncConflict, conflict_id)
|
||||
return _detach(db, row) if row else None
|
||||
|
||||
|
||||
def get_or_create_cursor(enterprise_id: str, farm_hub_id: str, direction: str) -> SyncCursor:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(SyncCursor).where(
|
||||
SyncCursor.farm_hub_id == farm_hub_id,
|
||||
SyncCursor.direction == direction,
|
||||
)
|
||||
)
|
||||
if row:
|
||||
return _detach(db, row)
|
||||
row = SyncCursor(
|
||||
enterprise_id=enterprise_id,
|
||||
farm_hub_id=farm_hub_id,
|
||||
direction=direction,
|
||||
last_acked_seq=0,
|
||||
last_pulled_seq=0,
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
return _detach(db, row)
|
||||
|
||||
|
||||
def update_cursor_ack(farm_hub_id: str, direction: str, last_acked_seq: int) -> None:
|
||||
with session_scope() as db:
|
||||
row = db.scalar(
|
||||
select(SyncCursor).where(
|
||||
SyncCursor.farm_hub_id == farm_hub_id,
|
||||
SyncCursor.direction == direction,
|
||||
)
|
||||
)
|
||||
if row:
|
||||
row.last_acked_seq = max(row.last_acked_seq, last_acked_seq)
|
||||
row.updated_at = datetime.now(UTC)
|
||||
|
||||
|
||||
def pull_events_since(
|
||||
enterprise_id: str,
|
||||
cursor: int,
|
||||
limit: int,
|
||||
*,
|
||||
exclude_origin_site_id: str | None = None,
|
||||
) -> list[SyncEventLog]:
|
||||
with session_scope() as db:
|
||||
query = select(SyncEventLog).where(
|
||||
SyncEventLog.enterprise_id == enterprise_id,
|
||||
SyncEventLog.seq > cursor,
|
||||
)
|
||||
if exclude_origin_site_id:
|
||||
query = query.where(
|
||||
(SyncEventLog.origin_site_id.is_(None))
|
||||
| (SyncEventLog.origin_site_id != exclude_origin_site_id)
|
||||
)
|
||||
rows = list(db.scalars(query.order_by(SyncEventLog.seq.asc()).limit(limit)))
|
||||
return [_detach(db, row) for row in rows]
|
||||
|
||||
|
||||
def get_farm_hub_site_id(farm_hub_id: str) -> str | None:
|
||||
with session_scope() as db:
|
||||
row = db.get(FarmHub, farm_hub_id)
|
||||
return row.hub_site_id if row else None
|
||||
Reference in New Issue
Block a user