journal/Journal.Core/Repositories/SqliteTodoRepository.cs
Jacob Schmidt c7933aeeec Lists & todos backend, editor refactor, inline create, UX improvements
- Wire up lists and todos to C# backend with full CRUD persistence
- Add models, DTOs, repositories, and services for lists and todo lists/items
- Preserve SQLite DB across vault rebuild/load cycles
- Add session store for vault password persistence across navigation
- Add inline name input for creating lists and todo lists in SidePanel
- Clear editor panel on section change with empty state placeholder
- Default markdown editor to preview mode on item selection
- Decompose EditorPanel into sub-components:
  - editor/FragmentEditor, editor/TodoEditor, editor/MarkdownEditor
  - Shared markdown utilities in utils/markdown.ts
- Strip verbose console/eprintln logging from frontend and Tauri backend
- Add graceful shutdown with vault persistence on window close

Co-Authored-By: Oz <oz-agent@warp.dev>
2026-02-26 17:33:27 -06:00

280 lines
9.7 KiB
C#

using Journal.Core.Models;
using Journal.Core.Services.Database;
using Microsoft.Data.Sqlite;
namespace Journal.Core.Repositories;
public sealed class SqliteTodoRepository(IDatabaseSessionService session) : ITodoRepository
{
private readonly IDatabaseSessionService _session = session;
// ── Lists ────────────────────────────────────────────────────────
public List<TodoList> GetAllLists()
{
var conn = _session.GetConnection();
return ReadAllLists(conn);
}
public TodoList? GetListById(Guid id)
{
var conn = _session.GetConnection();
return ReadListById(conn, id);
}
public void AddList(TodoList list)
{
ArgumentNullException.ThrowIfNull(list);
var conn = _session.GetConnection();
InsertList(conn, list);
}
public bool UpdateList(Guid id, string? label = null)
{
var conn = _session.GetConnection();
var existing = ReadListById(conn, id);
if (existing is null)
return false;
if (label is not null)
{
if (string.IsNullOrWhiteSpace(label))
throw new ArgumentException("Label cannot be empty", nameof(label));
existing.Label = label.Trim();
}
UpdateListRow(conn, existing);
return true;
}
public bool RemoveList(Guid id)
{
var conn = _session.GetConnection();
return DeleteList(conn, id);
}
// ── Items ────────────────────────────────────────────────────────
public List<TodoItem> GetItemsByListId(Guid listId)
{
var conn = _session.GetConnection();
return ReadItemsByListId(conn, listId);
}
public TodoItem? GetItemById(Guid id)
{
var conn = _session.GetConnection();
return ReadItemById(conn, id);
}
public void AddItem(TodoItem item)
{
ArgumentNullException.ThrowIfNull(item);
var conn = _session.GetConnection();
InsertItem(conn, item);
}
public bool UpdateItem(Guid id, string? text = null, bool? done = null, int? sortOrder = null)
{
var conn = _session.GetConnection();
var existing = ReadItemById(conn, id);
if (existing is null)
return false;
if (text is not null)
{
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException("Text cannot be empty", nameof(text));
existing.Text = text.Trim();
}
if (done.HasValue)
existing.Done = done.Value;
if (sortOrder.HasValue)
existing.SortOrder = sortOrder.Value;
UpdateItemRow(conn, existing);
return true;
}
public bool RemoveItem(Guid id)
{
var conn = _session.GetConnection();
return DeleteItem(conn, id);
}
// ── Private list helpers ─────────────────────────────────────────
private static void InsertList(SqliteConnection conn, TodoList list)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO todo_lists (guid, label, created_at)
VALUES (@guid, @label, @createdAt);
""";
cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D"));
cmd.Parameters.AddWithValue("@label", list.Label);
cmd.Parameters.AddWithValue("@createdAt", list.CreatedAt.ToString("O"));
cmd.ExecuteNonQuery();
}
private static void UpdateListRow(SqliteConnection conn, TodoList list)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "UPDATE todo_lists SET label = @label WHERE guid = @guid;";
cmd.Parameters.AddWithValue("@guid", list.Id.ToString("D"));
cmd.Parameters.AddWithValue("@label", list.Label);
cmd.ExecuteNonQuery();
}
private static bool DeleteList(SqliteConnection conn, Guid id)
{
using var tx = conn.BeginTransaction();
var rowId = GetListRowId(conn, id);
if (rowId.HasValue)
{
using var delItems = conn.CreateCommand();
delItems.CommandText = "DELETE FROM todo_items WHERE list_id = @listId;";
delItems.Parameters.AddWithValue("@listId", rowId.Value);
delItems.ExecuteNonQuery();
}
using var delList = conn.CreateCommand();
delList.CommandText = "DELETE FROM todo_lists WHERE guid = @guid;";
delList.Parameters.AddWithValue("@guid", id.ToString("D"));
var rows = delList.ExecuteNonQuery();
tx.Commit();
return rows > 0;
}
private static TodoList? ReadListById(SqliteConnection conn, Guid id)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists WHERE guid = @guid;";
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
using var reader = cmd.ExecuteReader();
return reader.Read() ? MapListRow(reader) : null;
}
private static List<TodoList> ReadAllLists(SqliteConnection conn)
{
var results = new List<TodoList>();
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT guid, label, created_at FROM todo_lists ORDER BY created_at;";
using var reader = cmd.ExecuteReader();
while (reader.Read())
results.Add(MapListRow(reader));
return results;
}
private static long? GetListRowId(SqliteConnection conn, Guid guid)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT id FROM todo_lists WHERE guid = @guid;";
cmd.Parameters.AddWithValue("@guid", guid.ToString("D"));
var result = cmd.ExecuteScalar();
return result is long id ? id : null;
}
private static TodoList MapListRow(SqliteDataReader reader)
{
var guid = Guid.Parse(reader.GetString(0));
var label = reader.GetString(1);
var createdAt = reader.IsDBNull(2) ? DateTimeOffset.MinValue : DateTimeOffset.Parse(reader.GetString(2));
return new TodoList(guid, label, createdAt);
}
// ── Private item helpers ─────────────────────────────────────────
private static void InsertItem(SqliteConnection conn, TodoItem item)
{
var listRowId = GetListRowId(conn, item.ListId)
?? throw new InvalidOperationException($"Todo list {item.ListId} not found");
using var cmd = conn.CreateCommand();
cmd.CommandText = """
INSERT INTO todo_items (guid, list_id, text, done, sort_order)
VALUES (@guid, @listId, @text, @done, @sortOrder);
""";
cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D"));
cmd.Parameters.AddWithValue("@listId", listRowId);
cmd.Parameters.AddWithValue("@text", item.Text);
cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0);
cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder);
cmd.ExecuteNonQuery();
}
private static void UpdateItemRow(SqliteConnection conn, TodoItem item)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
UPDATE todo_items SET text = @text, done = @done, sort_order = @sortOrder
WHERE guid = @guid;
""";
cmd.Parameters.AddWithValue("@guid", item.Id.ToString("D"));
cmd.Parameters.AddWithValue("@text", item.Text);
cmd.Parameters.AddWithValue("@done", item.Done ? 1 : 0);
cmd.Parameters.AddWithValue("@sortOrder", item.SortOrder);
cmd.ExecuteNonQuery();
}
private static bool DeleteItem(SqliteConnection conn, Guid id)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM todo_items WHERE guid = @guid;";
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
return cmd.ExecuteNonQuery() > 0;
}
private static TodoItem? ReadItemById(SqliteConnection conn, Guid id)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order
FROM todo_items ti
INNER JOIN todo_lists tl ON tl.id = ti.list_id
WHERE ti.guid = @guid;
""";
cmd.Parameters.AddWithValue("@guid", id.ToString("D"));
using var reader = cmd.ExecuteReader();
return reader.Read() ? MapItemRow(reader) : null;
}
private static List<TodoItem> ReadItemsByListId(SqliteConnection conn, Guid listId)
{
var results = new List<TodoItem>();
using var cmd = conn.CreateCommand();
cmd.CommandText = """
SELECT ti.guid, tl.guid, ti.text, ti.done, ti.sort_order
FROM todo_items ti
INNER JOIN todo_lists tl ON tl.id = ti.list_id
WHERE tl.guid = @listGuid
ORDER BY ti.sort_order, ti.guid;
""";
cmd.Parameters.AddWithValue("@listGuid", listId.ToString("D"));
using var reader = cmd.ExecuteReader();
while (reader.Read())
results.Add(MapItemRow(reader));
return results;
}
private static TodoItem MapItemRow(SqliteDataReader reader)
{
var guid = Guid.Parse(reader.GetString(0));
var listGuid = Guid.Parse(reader.GetString(1));
var text = reader.GetString(2);
var done = !reader.IsDBNull(3) && reader.GetInt64(3) != 0;
var sortOrder = reader.IsDBNull(4) ? 0 : (int)reader.GetInt64(4);
return new TodoItem(guid, listGuid, text, done, sortOrder);
}
}