Files
microdao-daarion/services/dao-service/repository_dao.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

457 lines
15 KiB
Python

"""
DAO Repository — Database operations for DAO
Phase 8: DAO Dashboard
"""
import uuid
from typing import List, Optional
from datetime import datetime
from decimal import Decimal
import asyncpg
from models import (
DaoCreate, DaoUpdate, DaoRead, DaoOverview,
DaoMember, MemberAdd,
DaoTreasuryItem, TreasuryUpdate
)
class DaoRepository:
def __init__(self, db_pool: asyncpg.Pool):
self.db = db_pool
# ========================================================================
# DAO — CRUD
# ========================================================================
async def create_dao(
self,
data: DaoCreate,
owner_user_id: str
) -> DaoRead:
"""Create new DAO and add owner as member"""
dao_id = uuid.uuid4()
async with self.db.acquire() as conn:
async with conn.transaction():
# Insert DAO
await conn.execute(
"""
INSERT INTO dao (
id, slug, name, description, microdao_id, owner_user_id,
governance_model, voting_period_seconds, quorum_percent
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
""",
dao_id,
data.slug,
data.name,
data.description,
uuid.UUID(data.microdao_id),
uuid.UUID(owner_user_id),
data.governance_model or 'simple',
data.voting_period_seconds or 604800,
data.quorum_percent or 20
)
# Add owner as member
await conn.execute(
"""
INSERT INTO dao_members (dao_id, user_id, role)
VALUES ($1, $2, $3)
""",
dao_id,
uuid.UUID(owner_user_id),
"owner"
)
# Log audit event
await conn.execute(
"""
INSERT INTO dao_audit_log (dao_id, actor_user_id, event_type, event_payload)
VALUES ($1, $2, $3, $4)
""",
dao_id,
uuid.UUID(owner_user_id),
"dao_created",
'{"slug": "' + data.slug + '"}'
)
# Get created DAO
return await self._get_dao_by_id(conn, dao_id)
async def update_dao(
self,
dao_id: str,
data: DaoUpdate
) -> Optional[DaoRead]:
"""Update DAO"""
updates = []
values = []
param_idx = 1
if data.name is not None:
updates.append(f"name = ${param_idx}")
values.append(data.name)
param_idx += 1
if data.description is not None:
updates.append(f"description = ${param_idx}")
values.append(data.description)
param_idx += 1
if data.governance_model is not None:
updates.append(f"governance_model = ${param_idx}")
values.append(data.governance_model)
param_idx += 1
if data.voting_period_seconds is not None:
updates.append(f"voting_period_seconds = ${param_idx}")
values.append(data.voting_period_seconds)
param_idx += 1
if data.quorum_percent is not None:
updates.append(f"quorum_percent = ${param_idx}")
values.append(data.quorum_percent)
param_idx += 1
if data.is_active is not None:
updates.append(f"is_active = ${param_idx}")
values.append(data.is_active)
param_idx += 1
if not updates:
return await self.get_dao_by_id(dao_id)
updates.append(f"updated_at = NOW()")
values.append(uuid.UUID(dao_id))
query = f"""
UPDATE dao
SET {', '.join(updates)}
WHERE id = ${param_idx}
RETURNING id
"""
async with self.db.acquire() as conn:
row = await conn.fetchrow(query, *values)
if not row:
return None
return await self._get_dao_by_id(conn, row['id'])
async def delete_dao(self, dao_id: str) -> bool:
"""Soft delete DAO (set is_active = false)"""
result = await self.db.execute(
"""
UPDATE dao
SET is_active = false, updated_at = NOW()
WHERE id = $1
""",
uuid.UUID(dao_id)
)
return result == "UPDATE 1"
async def get_dao_by_slug(self, slug: str) -> Optional[DaoRead]:
"""Get DAO by slug"""
async with self.db.acquire() as conn:
row = await conn.fetchrow(
"""
SELECT id, slug, name, description, microdao_id, owner_user_id,
governance_model, voting_period_seconds, quorum_percent,
is_active, created_at, updated_at
FROM dao
WHERE slug = $1
""",
slug
)
if not row:
return None
return self._row_to_dao(row)
async def get_dao_by_id(self, dao_id: str) -> Optional[DaoRead]:
"""Get DAO by ID"""
async with self.db.acquire() as conn:
return await self._get_dao_by_id(conn, uuid.UUID(dao_id))
async def _get_dao_by_id(
self,
conn: asyncpg.Connection,
dao_id: uuid.UUID
) -> Optional[DaoRead]:
"""Internal: get DAO by ID within transaction"""
row = await conn.fetchrow(
"""
SELECT id, slug, name, description, microdao_id, owner_user_id,
governance_model, voting_period_seconds, quorum_percent,
is_active, created_at, updated_at
FROM dao
WHERE id = $1
""",
dao_id
)
if not row:
return None
return self._row_to_dao(row)
async def list_daos_for_user(self, user_id: str) -> List[DaoRead]:
"""List DAOs where user is a member"""
rows = await self.db.fetch(
"""
SELECT d.id, d.slug, d.name, d.description, d.microdao_id, d.owner_user_id,
d.governance_model, d.voting_period_seconds, d.quorum_percent,
d.is_active, d.created_at, d.updated_at
FROM dao d
INNER JOIN dao_members dm ON dm.dao_id = d.id
WHERE dm.user_id = $1 AND d.is_active = true
ORDER BY d.created_at DESC
""",
uuid.UUID(user_id)
)
return [self._row_to_dao(row) for row in rows]
async def get_dao_overview(self, dao_id: str) -> Optional[DaoOverview]:
"""Get DAO overview with aggregated stats"""
dao = await self.get_dao_by_id(dao_id)
if not dao:
return None
# Get counts
async with self.db.acquire() as conn:
members_count = await conn.fetchval(
"SELECT COUNT(*) FROM dao_members WHERE dao_id = $1",
uuid.UUID(dao_id)
)
active_proposals_count = await conn.fetchval(
"SELECT COUNT(*) FROM dao_proposals WHERE dao_id = $1 AND status = 'active'",
uuid.UUID(dao_id)
)
total_proposals_count = await conn.fetchval(
"SELECT COUNT(*) FROM dao_proposals WHERE dao_id = $1",
uuid.UUID(dao_id)
)
# Get treasury items
treasury_items = await self.get_treasury_items(dao_id)
return DaoOverview(
dao=dao,
members_count=members_count or 0,
active_proposals_count=active_proposals_count or 0,
total_proposals_count=total_proposals_count or 0,
treasury_items=treasury_items
)
async def get_dao_overview_by_slug(self, slug: str) -> Optional[DaoOverview]:
"""Helper to get overview by slug"""
dao = await self.get_dao_by_slug(slug)
if not dao:
return None
return await self.get_dao_overview(dao.id)
def _row_to_dao(self, row: asyncpg.Record) -> DaoRead:
"""Convert database row to DaoRead"""
return DaoRead(
id=str(row['id']),
slug=row['slug'],
name=row['name'],
description=row['description'],
microdao_id=str(row['microdao_id']),
owner_user_id=str(row['owner_user_id']),
governance_model=row['governance_model'],
voting_period_seconds=row['voting_period_seconds'],
quorum_percent=row['quorum_percent'],
is_active=row['is_active'],
created_at=row['created_at'],
updated_at=row['updated_at']
)
# ========================================================================
# Members
# ========================================================================
async def list_members(self, dao_id: str) -> List[DaoMember]:
"""List all members of a DAO"""
rows = await self.db.fetch(
"""
SELECT id, dao_id, user_id, role, joined_at
FROM dao_members
WHERE dao_id = $1
ORDER BY joined_at ASC
""",
uuid.UUID(dao_id)
)
return [
DaoMember(
id=str(row['id']),
dao_id=str(row['dao_id']),
user_id=str(row['user_id']),
role=row['role'],
joined_at=row['joined_at']
)
for row in rows
]
async def add_member(
self,
dao_id: str,
user_id: str,
role: str = "member"
) -> DaoMember:
"""Add member to DAO"""
member_id = uuid.uuid4()
row = await self.db.fetchrow(
"""
INSERT INTO dao_members (id, dao_id, user_id, role)
VALUES ($1, $2, $3, $4)
ON CONFLICT (dao_id, user_id) DO UPDATE
SET role = EXCLUDED.role
RETURNING id, dao_id, user_id, role, joined_at
""",
member_id,
uuid.UUID(dao_id),
uuid.UUID(user_id),
role
)
return DaoMember(
id=str(row['id']),
dao_id=str(row['dao_id']),
user_id=str(row['user_id']),
role=row['role'],
joined_at=row['joined_at']
)
async def remove_member(self, member_id: str) -> bool:
"""Remove member from DAO"""
result = await self.db.execute(
"""
DELETE FROM dao_members
WHERE id = $1
""",
uuid.UUID(member_id)
)
return result == "DELETE 1"
async def get_user_role_in_dao(
self,
dao_id: str,
user_id: str
) -> Optional[str]:
"""Get user's role in DAO (or None if not a member)"""
row = await self.db.fetchrow(
"""
SELECT role FROM dao_members
WHERE dao_id = $1 AND user_id = $2
""",
uuid.UUID(dao_id),
uuid.UUID(user_id)
)
return row['role'] if row else None
# ========================================================================
# Treasury
# ========================================================================
async def get_treasury_items(self, dao_id: str) -> List[DaoTreasuryItem]:
"""Get all treasury items for a DAO"""
rows = await self.db.fetch(
"""
SELECT token_symbol, contract_address, balance
FROM dao_treasury
WHERE dao_id = $1
ORDER BY token_symbol
""",
uuid.UUID(dao_id)
)
return [
DaoTreasuryItem(
token_symbol=row['token_symbol'],
contract_address=row['contract_address'],
balance=row['balance']
)
for row in rows
]
async def apply_treasury_delta(
self,
dao_id: str,
token_symbol: str,
delta: Decimal
) -> DaoTreasuryItem:
"""Apply delta to treasury balance"""
async with self.db.acquire() as conn:
async with conn.transaction():
# Get current balance
row = await conn.fetchrow(
"""
SELECT balance FROM dao_treasury
WHERE dao_id = $1 AND token_symbol = $2
FOR UPDATE
""",
uuid.UUID(dao_id),
token_symbol
)
current_balance = row['balance'] if row else Decimal('0')
new_balance = current_balance + delta
if new_balance < 0:
raise ValueError(f"Insufficient balance: {current_balance} + {delta} = {new_balance} < 0")
# Upsert
row = await conn.fetchrow(
"""
INSERT INTO dao_treasury (dao_id, token_symbol, balance, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (dao_id, token_symbol) DO UPDATE
SET balance = $3, updated_at = NOW()
RETURNING token_symbol, contract_address, balance
""",
uuid.UUID(dao_id),
token_symbol,
new_balance
)
return DaoTreasuryItem(
token_symbol=row['token_symbol'],
contract_address=row['contract_address'],
balance=row['balance']
)
async def set_treasury_balance(
self,
dao_id: str,
token_symbol: str,
balance: Decimal
) -> DaoTreasuryItem:
"""Set treasury balance directly"""
if balance < 0:
raise ValueError(f"Balance cannot be negative: {balance}")
row = await self.db.fetchrow(
"""
INSERT INTO dao_treasury (dao_id, token_symbol, balance, updated_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (dao_id, token_symbol) DO UPDATE
SET balance = $3, updated_at = NOW()
RETURNING token_symbol, contract_address, balance
""",
uuid.UUID(dao_id),
token_symbol,
balance
)
return DaoTreasuryItem(
token_symbol=row['token_symbol'],
contract_address=row['contract_address'],
balance=row['balance']
)