north-scape/relayboard
Relayboard: accessible team boards with audited actions, dependency maps, and shared web/MCP use cases.
A remix template · 0 remixes · Remix north-scape/relayboard
app/boards/infrastructure/repository.py
"""DataK3 PostgreSQL-wire adapter. Board mutations and audit rows share one transaction.
FOR UPDATE protects the board aggregate (membership, cards, dependency graph). Connections
are request-local ContextVars, never shared between workers. No out-of-band database exists.
"""
import json
import threading
import uuid
from contextlib import contextmanager
from contextvars import ContextVar
import psycopg
from boards.domain.model import Board, Card, Conflict, Role
from boards.infrastructure.dodil import connect
_connection=ContextVar('relayboard_connection', default=None)
_slots=threading.BoundedSemaphore(8)
_CARD_COLUMNS='id, board_id, title, list_name, assignee, due_date, description, labels, blocked_by, revision, checklist'
def _card(row):
if row is None: return None
row=list(row)
row[7]=json.loads(row[7] or '[]')
row[8]=json.loads(row[8] or '[]')
row[10]=json.loads(row[10] or '[]')
return Card(*row)
class SqlBoards:
@contextmanager
def _db(self):
existing=_connection.get()
if existing is not None:
yield existing
else:
with _slots, connect() as c: yield c
@contextmanager
def atomic(self):
if _connection.get() is not None:
yield self
return
with _slots, connect() as c:
token=_connection.set(c)
try:
with c.transaction(): yield self
except psycopg.errors.SerializationFailure:
raise Conflict('another update happened; reload and retry') from None
finally: _connection.reset(token)
def get(self,board_id):
with self._db() as c:
suffix=' FOR UPDATE' if _connection.get() is not None else ''
row=c.execute('SELECT id, title, owner_email FROM boards WHERE id = %s'+suffix,(board_id,)).fetchone()
if not row: return None
if _connection.get() is not None:
# Touch the common aggregate guard, so disjoint dependency edits
# still conflict on optimistic DataK3 deployments.
c.execute('UPDATE boards SET revision = revision + 1 WHERE id = %s',(board_id,))
members=c.execute('SELECT email, role FROM memberships WHERE board_id = %s',(board_id,)).fetchall()
return Board(*row,members={e:Role(r) for e,r in members})
def boards_for(self,email):
with self._db() as c:
ids=[r[0] for r in c.execute('SELECT id FROM boards WHERE owner_email = %s UNION SELECT board_id FROM memberships WHERE email = %s',(email,email))]
return [b for b in (self.get(i) for i in ids) if b]
def save_board(self,board):
with self._db() as c:
exists=c.execute('SELECT id FROM boards WHERE id = %s',(board.id,)).fetchone()
if exists:
c.execute('UPDATE boards SET title = %s WHERE id = %s',(board.title,board.id))
else:
c.execute('INSERT INTO boards (id,title,owner_email,created_at) VALUES (%s,%s,%s,now())',(board.id,board.title,board.owner_email))
for email,role in board.members.items():
c.execute('INSERT INTO memberships (id,board_id,email,role) VALUES (%s,%s,%s,%s) ON CONFLICT (id) DO UPDATE SET role = EXCLUDED.role',
(f'{board.id}:{email}',board.id,email,role.value))
def get_card(self,id):
with self._db() as c:
return _card(c.execute('SELECT '+_CARD_COLUMNS+' FROM cards WHERE id = %s',(id,)).fetchone())
def delete_membership(self,board_id,email):
with self._db() as c:
c.execute('DELETE FROM memberships WHERE board_id = %s AND email = %s',(board_id,email))
def cards_on(self,id):
with self._db() as c:
return [_card(r) for r in c.execute('SELECT '+_CARD_COLUMNS+' FROM cards WHERE board_id = %s ORDER BY updated_at, id',(id,))]
def save_card(self,card):
values=(card.board_id,card.title,card.list_name,card.assignee,card.due_date,card.description,
json.dumps(card.labels),json.dumps(card.blocked_by),card.revision,json.dumps(card.checklist))
with self._db() as c:
exists=c.execute('SELECT id FROM cards WHERE id = %s',(card.id,)).fetchone()
if exists:
c.execute('UPDATE cards SET board_id=%s,title=%s,list_name=%s,assignee=%s,due_date=%s,description=%s,labels=%s,blocked_by=%s,revision=%s,checklist=%s,updated_at=now() WHERE id=%s',values+(card.id,))
else:
c.execute('INSERT INTO cards (board_id,title,list_name,assignee,due_date,description,labels,blocked_by,revision,checklist,id,updated_at) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,now())',values+(card.id,))
def log(self,board_id,card_id,actor,action,changes=None):
with self._db() as c:
c.execute('INSERT INTO activity (id,board_id,card_id,actor,action,changes,event_at) VALUES (%s,%s,%s,%s,%s,%s,now())',
(uuid.uuid4().hex,board_id,card_id,actor,action,json.dumps(changes or {})))
def activity(self,board_id):
with self._db() as c:
rows=c.execute('SELECT id,board_id,card_id,actor,action,changes,event_at FROM activity WHERE board_id = %s ORDER BY event_at DESC, id DESC LIMIT 100',(board_id,)).fetchall()
out=[]
for row in rows:
item=dict(zip(('id','board_id','card_id','actor','action','changes','event_at'),row))
item['changes']=json.loads(item['changes'] or '{}')
item['event_at']=item['event_at'].isoformat()
out.append(item)
return out