Initial commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
"""Postgres stub for VacationDaysRepository.
|
||||
|
||||
Implement when switching to Postgres (install asyncpg via `uv sync --extra postgres`).
|
||||
All methods raise NotImplementedError until then.
|
||||
"""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from app.entities.vacation_days import VacationDays
|
||||
from app.repositories.vacation_days_repository import VacationDaysRepository
|
||||
|
||||
|
||||
class PostgresVacationDaysRepository(VacationDaysRepository):
|
||||
def __init__(self, connection: object) -> None:
|
||||
self._conn = connection
|
||||
|
||||
async def create(self, item: VacationDays) -> VacationDays:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
|
||||
async def get_by_id(self, id: UUID) -> VacationDays | None:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
|
||||
async def update(self, id: UUID, item: VacationDays) -> VacationDays:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
|
||||
async def list(self, **filters: object) -> list[VacationDays]:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
|
||||
async def get_by_parent_year(self, personal_data_uuid: UUID, year: int) -> VacationDays | None:
|
||||
raise NotImplementedError("Postgres repo not yet implemented")
|
||||
@@ -0,0 +1,115 @@
|
||||
"""SQLite implementation of BreakPeriodRepository.
|
||||
|
||||
Handles the break_period table with date fields (start_date, end_date).
|
||||
"""
|
||||
|
||||
from datetime import date, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.break_period import BreakPeriod
|
||||
from app.repositories.break_period_repository import BreakPeriodRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteBreakPeriodRepository(BreakPeriodRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> BreakPeriod:
|
||||
return BreakPeriod(
|
||||
id=UUID(row["id"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
name=row["name"],
|
||||
start_date=date.fromisoformat(row["start_date"]),
|
||||
end_date=date.fromisoformat(row["end_date"]),
|
||||
target_weeks=row["target_weeks"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: BreakPeriod) -> BreakPeriod:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
item.updated_at = now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO break_period
|
||||
(id, workspace_id, name, start_date, end_date, target_weeks,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.workspace_id),
|
||||
item.name,
|
||||
item.start_date.isoformat(),
|
||||
item.end_date.isoformat(),
|
||||
item.target_weeks,
|
||||
item.created_at.isoformat(),
|
||||
item.updated_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> BreakPeriod | None:
|
||||
cursor = await self._conn.execute("SELECT * FROM break_period WHERE id = ?", (str(id),))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def get_by_workspace(self, workspace_id: UUID) -> list[BreakPeriod]:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM break_period WHERE workspace_id = ? ORDER BY start_date ASC",
|
||||
(str(workspace_id),),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(row) for row in rows]
|
||||
|
||||
async def update(self, id: UUID, item: BreakPeriod) -> BreakPeriod:
|
||||
item.updated_at = datetime.now()
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE break_period
|
||||
SET name = ?, start_date = ?, end_date = ?, target_weeks = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
item.name,
|
||||
item.start_date.isoformat(),
|
||||
item.end_date.isoformat(),
|
||||
item.target_weeks,
|
||||
item.updated_at.isoformat(),
|
||||
str(id),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"BreakPeriod {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None # just updated
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM break_period WHERE id = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"BreakPeriod {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[BreakPeriod]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
for column in ("workspace_id",):
|
||||
if column in filters and filters[column] is not None:
|
||||
value = filters[column]
|
||||
clauses.append(f"{column} = ?")
|
||||
params.append(str(value) if isinstance(value, UUID) else value)
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM break_period{where} ORDER BY start_date ASC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(row) for row in rows]
|
||||
@@ -0,0 +1,97 @@
|
||||
"""SQLite implementation of ChildRepository."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.child import Child
|
||||
from app.repositories.child_repository import ChildRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteChildRepository(ChildRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> Child:
|
||||
return Child(
|
||||
id=UUID(row["id"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
name=row["name"],
|
||||
birth_date=date.fromisoformat(row["birth_date"]),
|
||||
preschool_start_date=(
|
||||
date.fromisoformat(row["preschool_start_date"])
|
||||
if row["preschool_start_date"]
|
||||
else None
|
||||
),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: Child) -> Child:
|
||||
item.created_at = item.created_at or datetime.now()
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO child (id, workspace_id, name, birth_date, preschool_start_date, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.workspace_id),
|
||||
item.name,
|
||||
item.birth_date.isoformat(),
|
||||
item.preschool_start_date.isoformat() if item.preschool_start_date else None,
|
||||
item.created_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> Child | None:
|
||||
cursor = await self._conn.execute("SELECT * FROM child WHERE id = ?", (str(id),))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: Child) -> Child:
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE child
|
||||
SET name = ?, birth_date = ?, preschool_start_date = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
item.name,
|
||||
item.birth_date.isoformat(),
|
||||
item.preschool_start_date.isoformat() if item.preschool_start_date else None,
|
||||
str(id),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"Child {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM child WHERE id = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"Child {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[Child]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
if "workspace_id" in filters and filters["workspace_id"] is not None:
|
||||
clauses.append("workspace_id = ?")
|
||||
params.append(str(filters["workspace_id"]))
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM child{where} ORDER BY birth_date ASC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def list_by_workspace(self, workspace_id: UUID) -> list[Child]:
|
||||
return await self.list(workspace_id=workspace_id)
|
||||
@@ -0,0 +1,179 @@
|
||||
"""SQLite implementation of ParentalLeaveRepository."""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.parental_leave import ParentalLeaveFetch, TierData
|
||||
from app.repositories.parental_leave_repository import ParentalLeaveRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteParentalLeaveRepository(ParentalLeaveRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> ParentalLeaveFetch:
|
||||
raw = json.loads(row["tier_data"])
|
||||
tier_data = TierData(
|
||||
sjukpenning_total=raw["sjukpenning_total"],
|
||||
sjukpenning_used=raw["sjukpenning_used"],
|
||||
sjukpenning_left=raw["sjukpenning_left"],
|
||||
lagsta_niva_total=raw["lagsta_niva_total"],
|
||||
lagsta_niva_used=raw["lagsta_niva_used"],
|
||||
lagsta_niva_left=raw["lagsta_niva_left"],
|
||||
)
|
||||
return ParentalLeaveFetch(
|
||||
id=UUID(row["id"]),
|
||||
personal_data_uuid=UUID(row["personal_data_uuid"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
child_id=UUID(row["child_id"]),
|
||||
source=row["source"],
|
||||
fetched_at=datetime.fromisoformat(row["fetched_at"]),
|
||||
total_days=row["total_days"],
|
||||
days_used=row["days_used"],
|
||||
days_left=row["days_left"],
|
||||
reserved_days_total=row["reserved_days_total"],
|
||||
reserved_days_left=row["reserved_days_left"],
|
||||
transferable_days_left=row["transferable_days_left"],
|
||||
dubbeldagar_left=row["dubbeldagar_left"],
|
||||
tier_data=tier_data,
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _tier_data_to_json(td: TierData) -> str:
|
||||
return json.dumps(
|
||||
{
|
||||
"sjukpenning_total": td.sjukpenning_total,
|
||||
"sjukpenning_used": td.sjukpenning_used,
|
||||
"sjukpenning_left": td.sjukpenning_left,
|
||||
"lagsta_niva_total": td.lagsta_niva_total,
|
||||
"lagsta_niva_used": td.lagsta_niva_used,
|
||||
"lagsta_niva_left": td.lagsta_niva_left,
|
||||
}
|
||||
)
|
||||
|
||||
async def create(self, item: ParentalLeaveFetch) -> ParentalLeaveFetch:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
item.updated_at = item.updated_at or now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO parental_leave_fetch
|
||||
(id, personal_data_uuid, workspace_id, child_id, source, fetched_at,
|
||||
total_days, days_used, days_left, reserved_days_total, reserved_days_left,
|
||||
transferable_days_left, dubbeldagar_left, tier_data, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.personal_data_uuid),
|
||||
str(item.workspace_id),
|
||||
str(item.child_id),
|
||||
item.source,
|
||||
item.fetched_at.isoformat(),
|
||||
item.total_days,
|
||||
item.days_used,
|
||||
item.days_left,
|
||||
item.reserved_days_total,
|
||||
item.reserved_days_left,
|
||||
item.transferable_days_left,
|
||||
item.dubbeldagar_left,
|
||||
self._tier_data_to_json(item.tier_data),
|
||||
item.created_at.isoformat(),
|
||||
item.updated_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> ParentalLeaveFetch | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM parental_leave_fetch WHERE id = ?", (str(id),)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def get_latest_by_parent(
|
||||
self, personal_data_uuid: UUID, child_id: UUID
|
||||
) -> ParentalLeaveFetch | None:
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
SELECT * FROM parental_leave_fetch
|
||||
WHERE personal_data_uuid = ? AND child_id = ?
|
||||
ORDER BY fetched_at DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(str(personal_data_uuid), str(child_id)),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def list_history(
|
||||
self, personal_data_uuid: UUID, child_id: UUID
|
||||
) -> list[ParentalLeaveFetch]:
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
SELECT * FROM parental_leave_fetch
|
||||
WHERE personal_data_uuid = ? AND child_id = ?
|
||||
ORDER BY fetched_at DESC
|
||||
""",
|
||||
(str(personal_data_uuid), str(child_id)),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def update(self, id: UUID, item: ParentalLeaveFetch) -> ParentalLeaveFetch:
|
||||
item.updated_at = datetime.now()
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE parental_leave_fetch
|
||||
SET days_used = ?, days_left = ?, reserved_days_left = ?,
|
||||
transferable_days_left = ?, dubbeldagar_left = ?,
|
||||
tier_data = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
item.days_used,
|
||||
item.days_left,
|
||||
item.reserved_days_left,
|
||||
item.transferable_days_left,
|
||||
item.dubbeldagar_left,
|
||||
self._tier_data_to_json(item.tier_data),
|
||||
item.updated_at.isoformat(),
|
||||
str(id),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"ParentalLeaveFetch {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute(
|
||||
"DELETE FROM parental_leave_fetch WHERE id = ?", (str(id),)
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"ParentalLeaveFetch {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[ParentalLeaveFetch]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
for column in ("personal_data_uuid", "workspace_id", "child_id"):
|
||||
if column in filters and filters[column] is not None:
|
||||
clauses.append(f"{column} = ?")
|
||||
params.append(str(filters[column]))
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM parental_leave_fetch{where} ORDER BY fetched_at DESC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
@@ -0,0 +1,114 @@
|
||||
"""SQLite implementation of PersonalDataRepository.
|
||||
|
||||
Soft-delete: sets `deleted_at`; hard-delete: removes the row (CASCADE deletes
|
||||
all child rows via FK constraints).
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.personal_data import PersonalData
|
||||
from app.repositories.personal_data_repository import PersonalDataRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLitePersonalDataRepository(PersonalDataRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> PersonalData:
|
||||
return PersonalData(
|
||||
uuid=UUID(row["uuid"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
name=row["name"],
|
||||
salary_type=row["salary_type"],
|
||||
employment_status=row["employment_status"],
|
||||
personnummer_enc=row["personnummer_enc"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
deleted_at=datetime.fromisoformat(row["deleted_at"]) if row["deleted_at"] else None,
|
||||
)
|
||||
|
||||
async def create(self, item: PersonalData) -> PersonalData:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO personal_data
|
||||
(uuid, workspace_id, name, salary_type, employment_status,
|
||||
personnummer_enc, created_at, deleted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.uuid),
|
||||
str(item.workspace_id),
|
||||
item.name,
|
||||
item.salary_type,
|
||||
item.employment_status,
|
||||
item.personnummer_enc,
|
||||
item.created_at.isoformat(),
|
||||
item.deleted_at.isoformat() if item.deleted_at else None,
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> PersonalData | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM personal_data WHERE uuid = ? AND deleted_at IS NULL",
|
||||
(str(id),),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: PersonalData) -> PersonalData:
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE personal_data
|
||||
SET name = ?, salary_type = ?, employment_status = ?,
|
||||
personnummer_enc = ?
|
||||
WHERE uuid = ? AND deleted_at IS NULL
|
||||
""",
|
||||
(item.name, item.salary_type, item.employment_status, item.personnummer_enc, str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"PersonalData {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
await self.soft_delete(id)
|
||||
|
||||
async def soft_delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute(
|
||||
"UPDATE personal_data SET deleted_at = ? WHERE uuid = ? AND deleted_at IS NULL",
|
||||
(datetime.now().isoformat(), str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"PersonalData {id} not found")
|
||||
|
||||
async def hard_delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM personal_data WHERE uuid = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"PersonalData {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[PersonalData]:
|
||||
clauses = ["deleted_at IS NULL"]
|
||||
params: list[object] = []
|
||||
for column in ("workspace_id",):
|
||||
if column in filters and filters[column] is not None:
|
||||
clauses.append(f"{column} = ?")
|
||||
params.append(str(filters[column]))
|
||||
where = "WHERE " + " AND ".join(clauses)
|
||||
cursor = await self._conn.execute(f"SELECT * FROM personal_data {where}", params)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def list_by_workspace(self, workspace_id: UUID) -> list[PersonalData]:
|
||||
return await self.list(workspace_id=workspace_id)
|
||||
@@ -0,0 +1,161 @@
|
||||
"""SQLite implementation of PlannedLeaveRepository."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.planned_leave import PlannedLeaveDay
|
||||
from app.repositories.planned_leave_repository import PlannedLeaveRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLitePlannedLeaveRepository(PlannedLeaveRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> PlannedLeaveDay:
|
||||
return PlannedLeaveDay(
|
||||
id=UUID(row["id"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
child_id=UUID(row["child_id"]),
|
||||
parent_personal_data_uuid=UUID(row["parent_personal_data_uuid"]),
|
||||
date=date.fromisoformat(row["date"]),
|
||||
day_type=row["day_type"],
|
||||
is_dubbeldag=bool(row["is_dubbeldag"]),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: PlannedLeaveDay) -> PlannedLeaveDay:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
item.updated_at = item.updated_at or now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO planned_leave_day
|
||||
(id, workspace_id, child_id, parent_personal_data_uuid, date,
|
||||
day_type, is_dubbeldag, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.workspace_id),
|
||||
str(item.child_id),
|
||||
str(item.parent_personal_data_uuid),
|
||||
item.date.isoformat(),
|
||||
item.day_type,
|
||||
int(item.is_dubbeldag),
|
||||
item.created_at.isoformat(),
|
||||
item.updated_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> PlannedLeaveDay | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM planned_leave_day WHERE id = ?", (str(id),)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: PlannedLeaveDay) -> PlannedLeaveDay:
|
||||
item.updated_at = datetime.now()
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE planned_leave_day
|
||||
SET day_type = ?, is_dubbeldag = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(item.day_type, int(item.is_dubbeldag), item.updated_at.isoformat(), str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"PlannedLeaveDay {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM planned_leave_day WHERE id = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"PlannedLeaveDay {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[PlannedLeaveDay]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
for column in ("workspace_id", "child_id", "parent_personal_data_uuid"):
|
||||
if column in filters and filters[column] is not None:
|
||||
clauses.append(f"{column} = ?")
|
||||
params.append(str(filters[column]))
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM planned_leave_day{where} ORDER BY date ASC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def list_by_workspace_child(
|
||||
self,
|
||||
workspace_id: UUID,
|
||||
child_id: UUID,
|
||||
from_date: date | None = None,
|
||||
to_date: date | None = None,
|
||||
) -> list[PlannedLeaveDay]:
|
||||
clauses = ["workspace_id = ?", "child_id = ?"]
|
||||
params: list[object] = [str(workspace_id), str(child_id)]
|
||||
if from_date:
|
||||
clauses.append("date >= ?")
|
||||
params.append(from_date.isoformat())
|
||||
if to_date:
|
||||
clauses.append("date <= ?")
|
||||
params.append(to_date.isoformat())
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM planned_leave_day WHERE {' AND '.join(clauses)} ORDER BY date ASC",
|
||||
params,
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def count_by_parent_type(
|
||||
self,
|
||||
personal_data_uuid: UUID,
|
||||
child_id: UUID,
|
||||
day_type: str,
|
||||
from_date: date | None = None,
|
||||
to_date: date | None = None,
|
||||
) -> int:
|
||||
clauses = [
|
||||
"parent_personal_data_uuid = ?",
|
||||
"child_id = ?",
|
||||
"day_type = ?",
|
||||
]
|
||||
params: list[object] = [str(personal_data_uuid), str(child_id), day_type]
|
||||
if from_date:
|
||||
clauses.append("date >= ?")
|
||||
params.append(from_date.isoformat())
|
||||
if to_date:
|
||||
clauses.append("date <= ?")
|
||||
params.append(to_date.isoformat())
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT COUNT(*) FROM planned_leave_day WHERE {' AND '.join(clauses)}",
|
||||
params,
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
async def count_dubbeldagar(self, child_id: UUID, from_date: date | None = None) -> int:
|
||||
clauses = ["child_id = ?", "is_dubbeldag = 1"]
|
||||
params: list[object] = [str(child_id)]
|
||||
if from_date:
|
||||
clauses.append("date >= ?")
|
||||
params.append(from_date.isoformat())
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT COUNT(*) FROM planned_leave_day WHERE {' AND '.join(clauses)}",
|
||||
params,
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
@@ -0,0 +1,77 @@
|
||||
"""SQLite implementation of UserRepository."""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.user import User
|
||||
from app.repositories.user_repository import UserRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteUserRepository(UserRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> User:
|
||||
return User(
|
||||
id=UUID(row["id"]),
|
||||
personal_data_uuid=UUID(row["personal_data_uuid"]),
|
||||
email=row["email"],
|
||||
hashed_password=row["hashed_password"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: User) -> User:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO "user" (id, personal_data_uuid, email, hashed_password, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.personal_data_uuid),
|
||||
item.email,
|
||||
item.hashed_password,
|
||||
item.created_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> User | None:
|
||||
cursor = await self._conn.execute('SELECT * FROM "user" WHERE id = ?', (str(id),))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def get_by_email(self, email: str) -> User | None:
|
||||
cursor = await self._conn.execute('SELECT * FROM "user" WHERE email = ?', (email,))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: User) -> User:
|
||||
cursor = await self._conn.execute(
|
||||
'UPDATE "user" SET email = ?, hashed_password = ? WHERE id = ?',
|
||||
(item.email, item.hashed_password, str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"User {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute('DELETE FROM "user" WHERE id = ?', (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"User {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[User]:
|
||||
cursor = await self._conn.execute('SELECT * FROM "user"')
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
@@ -0,0 +1,111 @@
|
||||
"""SQLite implementation of VacationDaysRepository.
|
||||
|
||||
REFERENCE implementation — copy this structure for other entities' SQLite repos.
|
||||
Notes for whoever extends this:
|
||||
- Map rows <-> entities explicitly in `_row_to_entity` (no ORM magic).
|
||||
- Always parameterize queries (never string-format SQL).
|
||||
- Raise NotFoundError from update/delete when the row is missing.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.vacation_days import VacationDays
|
||||
from app.repositories.vacation_days_repository import VacationDaysRepository
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteVacationDaysRepository(VacationDaysRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> VacationDays:
|
||||
return VacationDays(
|
||||
id=UUID(row["id"]),
|
||||
personal_data_uuid=UUID(row["personal_data_uuid"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
year=row["year"],
|
||||
total_days=row["total_days"],
|
||||
saved_days_in=row["saved_days_in"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
updated_at=datetime.fromisoformat(row["updated_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: VacationDays) -> VacationDays:
|
||||
now = datetime.now()
|
||||
item.created_at = item.created_at or now
|
||||
item.updated_at = now
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO vacation_days
|
||||
(id, personal_data_uuid, workspace_id, year, total_days,
|
||||
saved_days_in, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.personal_data_uuid),
|
||||
str(item.workspace_id),
|
||||
item.year,
|
||||
item.total_days,
|
||||
item.saved_days_in,
|
||||
item.created_at.isoformat(),
|
||||
item.updated_at.isoformat(),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> VacationDays | None:
|
||||
cursor = await self._conn.execute("SELECT * FROM vacation_days WHERE id = ?", (str(id),))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def get_by_parent_year(self, personal_data_uuid: UUID, year: int) -> VacationDays | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM vacation_days WHERE personal_data_uuid = ? AND year = ?",
|
||||
(str(personal_data_uuid), year),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: VacationDays) -> VacationDays:
|
||||
item.updated_at = datetime.now()
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE vacation_days
|
||||
SET total_days = ?, saved_days_in = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(item.total_days, item.saved_days_in, item.updated_at.isoformat(), str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"VacationDays {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None # just updated
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM vacation_days WHERE id = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"VacationDays {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[VacationDays]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
for column in ("workspace_id", "personal_data_uuid", "year"):
|
||||
if column in filters and filters[column] is not None:
|
||||
value = filters[column]
|
||||
clauses.append(f"{column} = ?")
|
||||
params.append(str(value) if isinstance(value, UUID) else value)
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM vacation_days{where} ORDER BY year DESC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(row) for row in rows]
|
||||
@@ -0,0 +1,168 @@
|
||||
"""SQLite implementation of WorkspaceRepository and WorkspaceInvitationRepository."""
|
||||
|
||||
from datetime import datetime
|
||||
from uuid import UUID
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.entities.workspace import Workspace
|
||||
from app.entities.workspace_invitation import WorkspaceInvitation
|
||||
from app.repositories.workspace_repository import (
|
||||
WorkspaceInvitationRepository,
|
||||
WorkspaceRepository,
|
||||
)
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
class SQLiteWorkspaceRepository(WorkspaceRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> Workspace:
|
||||
return Workspace(
|
||||
id=UUID(row["id"]),
|
||||
name=row["name"],
|
||||
planning_goal=row["planning_goal"],
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
)
|
||||
|
||||
async def create(self, item: Workspace) -> Workspace:
|
||||
item.created_at = item.created_at or datetime.now()
|
||||
await self._conn.execute(
|
||||
"INSERT INTO workspace (id, name, planning_goal, created_at) VALUES (?, ?, ?, ?)",
|
||||
(str(item.id), item.name, item.planning_goal, item.created_at.isoformat()),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> Workspace | None:
|
||||
cursor = await self._conn.execute("SELECT * FROM workspace WHERE id = ?", (str(id),))
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: Workspace) -> Workspace:
|
||||
cursor = await self._conn.execute(
|
||||
"UPDATE workspace SET name = ?, planning_goal = ? WHERE id = ?",
|
||||
(item.name, item.planning_goal, str(id)),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"Workspace {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute("DELETE FROM workspace WHERE id = ?", (str(id),))
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"Workspace {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[Workspace]:
|
||||
cursor = await self._conn.execute("SELECT * FROM workspace")
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
|
||||
class SQLiteWorkspaceInvitationRepository(WorkspaceInvitationRepository):
|
||||
def __init__(self, connection: aiosqlite.Connection) -> None:
|
||||
self._conn = connection
|
||||
|
||||
@staticmethod
|
||||
def _row_to_entity(row: aiosqlite.Row) -> WorkspaceInvitation:
|
||||
return WorkspaceInvitation(
|
||||
id=UUID(row["id"]),
|
||||
workspace_id=UUID(row["workspace_id"]),
|
||||
token_hash=row["token_hash"],
|
||||
invited_by_user_id=UUID(row["invited_by_user_id"]),
|
||||
status=row["status"],
|
||||
expires_at=datetime.fromisoformat(row["expires_at"]),
|
||||
created_at=datetime.fromisoformat(row["created_at"]),
|
||||
accepted_at=datetime.fromisoformat(row["accepted_at"]) if row["accepted_at"] else None,
|
||||
)
|
||||
|
||||
async def create(self, item: WorkspaceInvitation) -> WorkspaceInvitation:
|
||||
await self._conn.execute(
|
||||
"""
|
||||
INSERT INTO workspace_invitation
|
||||
(id, workspace_id, token_hash, invited_by_user_id, status,
|
||||
expires_at, created_at, accepted_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
str(item.id),
|
||||
str(item.workspace_id),
|
||||
item.token_hash,
|
||||
str(item.invited_by_user_id),
|
||||
item.status,
|
||||
item.expires_at.isoformat(),
|
||||
item.created_at.isoformat(),
|
||||
item.accepted_at.isoformat() if item.accepted_at else None,
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
return item
|
||||
|
||||
async def get_by_id(self, id: UUID) -> WorkspaceInvitation | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM workspace_invitation WHERE id = ?", (str(id),)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def get_by_token_hash(self, token_hash: str) -> WorkspaceInvitation | None:
|
||||
cursor = await self._conn.execute(
|
||||
"SELECT * FROM workspace_invitation WHERE token_hash = ?", (token_hash,)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return self._row_to_entity(row) if row else None
|
||||
|
||||
async def update(self, id: UUID, item: WorkspaceInvitation) -> WorkspaceInvitation:
|
||||
cursor = await self._conn.execute(
|
||||
"""
|
||||
UPDATE workspace_invitation
|
||||
SET status = ?, accepted_at = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(
|
||||
item.status,
|
||||
item.accepted_at.isoformat() if item.accepted_at else None,
|
||||
str(id),
|
||||
),
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"WorkspaceInvitation {id} not found")
|
||||
refreshed = await self.get_by_id(id)
|
||||
assert refreshed is not None
|
||||
return refreshed
|
||||
|
||||
async def delete(self, id: UUID) -> None:
|
||||
cursor = await self._conn.execute(
|
||||
"DELETE FROM workspace_invitation WHERE id = ?", (str(id),)
|
||||
)
|
||||
await self._conn.commit()
|
||||
if cursor.rowcount == 0:
|
||||
raise NotFoundError(f"WorkspaceInvitation {id} not found")
|
||||
|
||||
async def list(self, **filters: object) -> list[WorkspaceInvitation]:
|
||||
clauses: list[str] = []
|
||||
params: list[object] = []
|
||||
if "workspace_id" in filters and filters["workspace_id"] is not None:
|
||||
clauses.append("workspace_id = ?")
|
||||
params.append(str(filters["workspace_id"]))
|
||||
if "status" in filters and filters["status"] is not None:
|
||||
clauses.append("status = ?")
|
||||
params.append(filters["status"])
|
||||
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
cursor = await self._conn.execute(
|
||||
f"SELECT * FROM workspace_invitation{where} ORDER BY created_at DESC", params
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [self._row_to_entity(r) for r in rows]
|
||||
|
||||
async def list_by_workspace(
|
||||
self, workspace_id: UUID, status: str | None = None
|
||||
) -> list[WorkspaceInvitation]:
|
||||
return await self.list(workspace_id=workspace_id, status=status)
|
||||
Reference in New Issue
Block a user