munch-ease-backend/households/repository.py

188 lines
5.9 KiB
Python
Raw Permalink Normal View History

2025-11-05 09:24:47 +00:00
from __future__ import annotations
from typing import List
from households.models import Household, HouseholdMember, HouseholdInvitation
async def create(conn):
# Households table
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS Household (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE
);
"""
)
# Membership table
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS HouseholdMember (
user_id INTEGER NOT NULL,
household_id INTEGER NOT NULL,
role TEXT NOT NULL,
PRIMARY KEY (user_id, household_id),
FOREIGN KEY(user_id) REFERENCES User(id) ON DELETE CASCADE,
FOREIGN KEY(household_id) REFERENCES Household(id) ON DELETE CASCADE
);
"""
)
# Invitations table
await conn.execute(
"""
CREATE TABLE IF NOT EXISTS HouseholdInvitation (
id INTEGER PRIMARY KEY,
household_id INTEGER NOT NULL,
invited_by_user_id INTEGER NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
status TEXT NOT NULL,
FOREIGN KEY(household_id) REFERENCES Household(id) ON DELETE CASCADE,
FOREIGN KEY(invited_by_user_id) REFERENCES User(id) ON DELETE SET NULL
);
"""
)
# Indices
2025-11-01 04:34:01 +00:00
await conn.execute("CREATE INDEX IF NOT EXISTS idx_household_slug ON Household(slug);")
await conn.execute(
"CREATE INDEX IF NOT EXISTS idx_household_member_household ON HouseholdMember(household_id);"
)
2025-11-02 06:45:12 +00:00
async def member_ids_in_household(conn, household_id: int, user_ids: list[int]) -> set[int]:
"""Return the subset of user_ids that are members of the given household.
Uses HouseholdMember join User to ensure users exist.
"""
if not user_ids:
return set()
placeholders = ",".join(["?"] * len(user_ids))
query = f"""
SELECT u.id
FROM HouseholdMember hm
JOIN User u ON u.id = hm.user_id
WHERE hm.household_id = ? AND u.id IN ({placeholders})
"""
valid: set[int] = set()
async with conn.execute(query, (household_id, *sorted(user_ids))) as c:
async for row in c:
valid.add(int(row[0]))
return valid
async def are_members(conn, household_id: int, user_ids: list[int]) -> bool:
"""True only if all provided user_ids are members of the household."""
if not user_ids:
return True
found = await member_ids_in_household(conn, household_id, user_ids)
return found == set(user_ids)
2025-11-05 09:24:47 +00:00
async def list_for_user(conn, user_id: int) -> List[Household]:
results: List[Household] = []
cols = ", ".join([f"h.{k}" for k in Household.KEYS])
2025-11-05 09:24:47 +00:00
async with conn.execute(
f"""
SELECT {cols} FROM Household h
2025-11-05 09:24:47 +00:00
JOIN HouseholdMember m ON m.household_id = h.id
WHERE m.user_id = ?
ORDER BY h.id
""",
(user_id,),
) as c:
async for row in c:
results.append(Household(**{k: v for k, v in zip(Household.KEYS, row)}))
2025-11-05 09:24:47 +00:00
return results
async def create_for_user(conn, name: str, slug: str, user_id: int) -> Household | None:
try:
async with conn.execute(
"INSERT INTO Household (name, slug) VALUES (?, ?)", (name, slug)
) as cur:
lrid = cur.lastrowid
if lrid is None:
return None
hid = int(lrid)
await conn.execute(
"INSERT INTO HouseholdMember (user_id, household_id, role) VALUES (?, ?, ?)",
(user_id, hid, "admin"),
)
return Household(id=hid, name=name, slug=slug)
except Exception:
return None
async def list_members(conn, household_id: int) -> List[HouseholdMember]:
members: List[HouseholdMember] = []
cols = ", ".join([f"u.{k}" for k in ["id", "display_name"]] + ["m.role"])
2025-11-05 09:24:47 +00:00
async with conn.execute(
f"""
SELECT {cols}
2025-11-05 09:24:47 +00:00
FROM HouseholdMember m
JOIN User u ON u.id = m.user_id
WHERE m.household_id = ?
ORDER BY lower(u.display_name), u.id
""",
(household_id,),
) as c:
async for row in c:
members.append(
HouseholdMember(**{k: v for k, v in zip(HouseholdMember.KEYS, row)})
2025-11-05 09:24:47 +00:00
)
return members
async def create_invitation(
2025-11-05 09:27:59 +00:00
conn, household_id: int, invited_by_user_id: int, token: str, expires_at: str
2025-11-05 09:24:47 +00:00
) -> bool:
try:
await conn.execute(
"""
2025-11-05 09:27:59 +00:00
INSERT INTO HouseholdInvitation (household_id, invited_by_user_id, token, expires_at, status)
VALUES (?, ?, ?, ?, ?)
2025-11-05 09:24:47 +00:00
""",
2025-11-05 09:27:59 +00:00
(household_id, invited_by_user_id, token, expires_at, "pending"),
2025-11-05 09:24:47 +00:00
)
return True
except Exception:
return False
async def get_invitation_by_token(conn, token: str) -> HouseholdInvitation | None:
cols = ", ".join(HouseholdInvitation.KEYS)
2025-11-05 09:24:47 +00:00
async with conn.execute(
f"SELECT {cols} FROM HouseholdInvitation WHERE token = ?",
2025-11-05 09:24:47 +00:00
(token,),
) as c:
row = await c.fetchone()
if not row:
return None
return HouseholdInvitation(**{k: v for k, v in zip(HouseholdInvitation.KEYS, row)})
2025-11-05 09:24:47 +00:00
async def accept_invitation(conn, invitation_id: int, user_id: int, household_id: int):
await conn.execute(
"INSERT OR IGNORE INTO HouseholdMember (user_id, household_id, role) VALUES (?, ?, ?)",
(user_id, household_id, "member"),
)
await conn.execute(
"UPDATE HouseholdInvitation SET status = 'accepted' WHERE id = ?",
(invitation_id,),
)
async def get_household_by_id(conn, household_id: int) -> Household | None:
cols = ", ".join(Household.KEYS)
2025-11-05 09:24:47 +00:00
async with conn.execute(
f"SELECT {cols} FROM Household WHERE id = ?",
2025-11-05 09:24:47 +00:00
(household_id,),
) as c:
row = await c.fetchone()
if not row:
return None
return Household(**{k: v for k, v in zip(Household.KEYS, row)})