Add the database schema and repository layer

The per-calendar JSON blob is gone. v1 kept every calendar's colour, every
visibility toggle and a custom palette inside one calendar_colors TEXT
column, so hiding one calendar rewrote the whole document -- which is where
"Fix calendar visibility preservation during event updates" came from.
Those are rows now, and set_visible touches visibility alone.

Foreign keys are switched on. v1 declared external_calendars.user_id as
INTEGER against a TEXT users.id; SQLite enforces neither the type nor the
constraint unless asked, so it was decorative and could never match.

Session tokens are stored as SHA-256, never in the clear, so a copy of the
database cannot be used to impersonate anyone. The CalDAV password is the
one secret that cannot be hashed -- it has to be replayed to the server --
so it gets an encrypted column with the algorithm recorded alongside, and
one way in and one way out instead of v1's eight localStorage reads.

Preferences are one column each with CHECK constraints, so a bad view or a
nonsense time increment is refused whatever route it arrives by. A NULL
display timezone means "follow the browser", which is a state v1 could not
express -- as with a NULL calendar colour meaning "defer to the server",
which is why it hashed paths to invent one.

Feed caching stores a content hash beside the ETag, because a published
Outlook feed sends neither ETag nor Last-Modified and staleness has to be
detectable anyway.

Thirty tests against real in-memory SQLite, not mocks. One caught that
create() returned nanosecond timestamps while the column stores
microseconds, so a session never compared equal to itself read back.
This commit is contained in:
2026-08-26 15:54:28 -04:00
parent c197e08af0
commit 82d05dc67a
15 changed files with 2206 additions and 5 deletions
+3
View File
@@ -40,6 +40,9 @@ uuid = { version = "1", features = ["v4", "serde"] }
icalendar = "0.17"
rrule = "0.14"
# Crypto
sha2 = "0.10"
# Errors + logging
thiserror = "2"
tracing = "0.1"