Remove dead code, fix duplicated connection logic, eliminate fake async
- Delete unused FileFragmentRepository.cs - DatabaseSessionService delegates to JournalDatabaseService for OpenEncryptedConnection/EnsureSchema instead of duplicating logic - Make IJournalDatabaseService expose OpenEncryptedConnection and EnsureSchema as public - Convert entire fragment chain from fake async (Task.FromResult wrappers) to synchronous: IFragmentRepository, SqliteFragmentRepository, InMemoryFragmentRepository, IFragmentService, FragmentService, Entry.cs dispatch, and SmokeTests Co-Authored-By: Warp <agent@warp.dev>
This commit is contained in:
parent
e85a3f6d26
commit
f86ac5e4b9
@ -81,18 +81,18 @@ public class Entry(
|
||||
switch (action)
|
||||
{
|
||||
case "fragments.list":
|
||||
result = await _fragments.GetAllAsync();
|
||||
result = _fragments.GetAll();
|
||||
break;
|
||||
case "fragments.get":
|
||||
if (!Guid.TryParse(cmd.Id, out var getId))
|
||||
return Error("Invalid or missing id");
|
||||
result = await _fragments.GetByIdAsync(getId);
|
||||
result = _fragments.GetById(getId);
|
||||
break;
|
||||
case "fragments.create":
|
||||
var createDto = DeserializePayload<CreateFragmentDto>(cmd.Payload);
|
||||
if (createDto is null)
|
||||
return Error("Missing or invalid payload");
|
||||
result = await _fragments.CreateAsync(createDto);
|
||||
result = _fragments.Create(createDto);
|
||||
break;
|
||||
case "fragments.update":
|
||||
if (!Guid.TryParse(cmd.Id, out var updateId))
|
||||
@ -100,15 +100,15 @@ public class Entry(
|
||||
var updateDto = DeserializePayload<UpdateFragmentDto>(cmd.Payload);
|
||||
if (updateDto is null)
|
||||
return Error("Missing or invalid payload");
|
||||
result = await _fragments.UpdateAsync(updateId, updateDto);
|
||||
result = _fragments.Update(updateId, updateDto);
|
||||
break;
|
||||
case "fragments.delete":
|
||||
if (!Guid.TryParse(cmd.Id, out var deleteId))
|
||||
return Error("Invalid or missing id");
|
||||
result = await _fragments.RemoveAsync(deleteId);
|
||||
result = _fragments.Remove(deleteId);
|
||||
break;
|
||||
case "fragments.search":
|
||||
result = await _fragments.SearchAsync(cmd.Type, cmd.Tag);
|
||||
result = _fragments.Search(cmd.Type, cmd.Tag);
|
||||
break;
|
||||
case "search.entries":
|
||||
var searchPayload = DeserializePayload<SearchEntriesPayload>(cmd.Payload);
|
||||
|
||||
@ -1,228 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Journal.Core.Models;
|
||||
|
||||
namespace Journal.Core.Repositories;
|
||||
|
||||
public class FileFragmentRepository : IFragmentRepository
|
||||
{
|
||||
private readonly Lock _lock = new();
|
||||
private readonly string _storagePath;
|
||||
private readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
private readonly List<Fragment> _store;
|
||||
|
||||
public FileFragmentRepository() : this(storagePath: null)
|
||||
{
|
||||
}
|
||||
|
||||
public FileFragmentRepository(string? storagePath)
|
||||
{
|
||||
_storagePath = ResolveStoragePath(storagePath);
|
||||
_store = LoadStore(_storagePath);
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetAllAsync()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Fragment?> GetByIdAsync(Guid id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.FirstOrDefault(f => f.Id == id));
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddAsync(Fragment fragment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fragment);
|
||||
lock (_lock)
|
||||
{
|
||||
Normalize(fragment);
|
||||
_store.Add(fragment);
|
||||
SaveStoreLocked();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(Guid id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var item = _store.FirstOrDefault(f => f.Id == id);
|
||||
if (item is null)
|
||||
return Task.FromResult(false);
|
||||
|
||||
var removed = _store.Remove(item);
|
||||
if (removed)
|
||||
SaveStoreLocked();
|
||||
return Task.FromResult(removed);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> UpdateAsync(
|
||||
Guid id,
|
||||
string? type = null,
|
||||
string? description = null,
|
||||
IEnumerable<string>? tags = null,
|
||||
DateTimeOffset? time = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var item = _store.FirstOrDefault(f => f.Id == id);
|
||||
if (item is null)
|
||||
return Task.FromResult(false);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
throw new ArgumentException("Type cannot be empty", nameof(type));
|
||||
item.Type = type.Trim();
|
||||
}
|
||||
|
||||
if (description != null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(description))
|
||||
throw new ArgumentException("Description cannot be empty", nameof(description));
|
||||
item.Description = description.Trim();
|
||||
}
|
||||
|
||||
if (tags != null)
|
||||
{
|
||||
item.Tags = [..
|
||||
tags.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Select(t => t.Trim())];
|
||||
}
|
||||
|
||||
if (time.HasValue)
|
||||
item.Time = time.Value;
|
||||
|
||||
SaveStoreLocked();
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTagAsync(string tag)
|
||||
{
|
||||
var q = tag?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return Task.FromResult(new List<Fragment>());
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var items = _store
|
||||
.Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTypeAsync(string type)
|
||||
{
|
||||
var q = type?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return Task.FromResult(new List<Fragment>());
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var items = _store
|
||||
.Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
{
|
||||
var qType = type?.Trim();
|
||||
var qTag = tag?.Trim();
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
IEnumerable<Fragment> results = _store;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(qType))
|
||||
results = results.Where(f => f.Type.Contains(qType, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(qTag))
|
||||
results = results.Where(f => f.Tags.Any(t => t.Contains(qTag, StringComparison.OrdinalIgnoreCase)));
|
||||
if (timeAfter.HasValue)
|
||||
results = results.Where(f => f.Time > timeAfter.Value);
|
||||
|
||||
return Task.FromResult(results.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveStoragePath(string? storagePath)
|
||||
{
|
||||
var configured = storagePath;
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
configured = Environment.GetEnvironmentVariable("JOURNAL_FRAGMENT_STORE_PATH");
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
configured = Path.Combine(Environment.CurrentDirectory, ".journal-sidecar", "fragments.json");
|
||||
|
||||
return Path.GetFullPath(configured);
|
||||
}
|
||||
|
||||
private List<Fragment> LoadStore(string path)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
if (!File.Exists(path))
|
||||
return [];
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return [];
|
||||
|
||||
var docs = JsonSerializer.Deserialize<List<FragmentDocument>>(json, _jsonOptions) ?? [];
|
||||
return [.. docs.Select(d => new Fragment(d.Id, d.Type, d.Description, d.Time, d.Tags))];
|
||||
}
|
||||
|
||||
private void SaveStoreLocked()
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_storagePath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var docs = _store.Select(f => new FragmentDocument
|
||||
{
|
||||
Id = f.Id,
|
||||
Type = f.Type,
|
||||
Description = f.Description,
|
||||
Time = f.Time,
|
||||
Tags = [.. f.Tags]
|
||||
}).ToList();
|
||||
var json = JsonSerializer.Serialize(docs, _jsonOptions);
|
||||
|
||||
var tempPath = _storagePath + ".tmp";
|
||||
File.WriteAllText(tempPath, json);
|
||||
File.Copy(tempPath, _storagePath, overwrite: true);
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
|
||||
private static void Normalize(Fragment fragment)
|
||||
{
|
||||
fragment.Type = fragment.Type.Trim();
|
||||
fragment.Description = fragment.Description.Trim();
|
||||
fragment.Tags = [..
|
||||
fragment.Tags.Where(t => !string.IsNullOrWhiteSpace(t))
|
||||
.Select(t => t.Trim())];
|
||||
}
|
||||
|
||||
private sealed class FragmentDocument
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public string Type { get; init; } = "";
|
||||
public string Description { get; init; } = "";
|
||||
public DateTimeOffset Time { get; init; }
|
||||
public List<string> Tags { get; init; } = [];
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,12 @@ namespace Journal.Core.Repositories;
|
||||
|
||||
public interface IFragmentRepository
|
||||
{
|
||||
Task<List<Fragment>> GetAllAsync();
|
||||
Task<Fragment?> GetByIdAsync(Guid id);
|
||||
Task AddAsync(Fragment fragment);
|
||||
Task<bool> RemoveAsync(Guid id);
|
||||
Task<bool> UpdateAsync(Guid id, string? type = null, string? description = null, IEnumerable<string>? tags = null, DateTimeOffset? time = null);
|
||||
Task<List<Fragment>> GetByTagAsync(string tag);
|
||||
Task<List<Fragment>> GetByTypeAsync(string type);
|
||||
Task<List<Fragment>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null);
|
||||
List<Fragment> GetAll();
|
||||
Fragment? GetById(Guid id);
|
||||
void Add(Fragment fragment);
|
||||
bool Remove(Guid id);
|
||||
bool Update(Guid id, string? type = null, string? description = null, IEnumerable<string>? tags = null, DateTimeOffset? time = null);
|
||||
List<Fragment> GetByTag(string tag);
|
||||
List<Fragment> GetByType(string type);
|
||||
List<Fragment> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null);
|
||||
}
|
||||
|
||||
@ -7,23 +7,23 @@ public class InMemoryFragmentRepository : IFragmentRepository
|
||||
private readonly List<Fragment> _store = [];
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public Task<List<Fragment>> GetAllAsync()
|
||||
public List<Fragment> GetAll()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.ToList());
|
||||
return _store.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Fragment?> GetByIdAsync(Guid id)
|
||||
public Fragment? GetById(Guid id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.FirstOrDefault(f => f.Id == id));
|
||||
return _store.FirstOrDefault(f => f.Id == id);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AddAsync(Fragment fragment)
|
||||
public void Add(Fragment fragment)
|
||||
{
|
||||
if (fragment is null) throw new ArgumentNullException(nameof(fragment));
|
||||
lock (_lock)
|
||||
@ -39,25 +39,24 @@ public class InMemoryFragmentRepository : IFragmentRepository
|
||||
|
||||
_store.Add(fragment);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(Guid id)
|
||||
public bool Remove(Guid id)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var item = _store.FirstOrDefault(f => f.Id == id);
|
||||
if (item is null) return Task.FromResult(false);
|
||||
return Task.FromResult(_store.Remove(item));
|
||||
if (item is null) return false;
|
||||
return _store.Remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> UpdateAsync(Guid id, string? type = null, string? description = null, IEnumerable<string>? tags = null, DateTimeOffset? time = null)
|
||||
public bool Update(Guid id, string? type = null, string? description = null, IEnumerable<string>? tags = null, DateTimeOffset? time = null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var item = _store.FirstOrDefault(f => f.Id == id);
|
||||
if (item is null) return Task.FromResult(false);
|
||||
if (item is null) return false;
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
@ -81,31 +80,31 @@ public class InMemoryFragmentRepository : IFragmentRepository
|
||||
if (time.HasValue)
|
||||
item.Time = time.Value;
|
||||
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTagAsync(string tag)
|
||||
public List<Fragment> GetByTag(string tag)
|
||||
{
|
||||
var q = tag?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q)) return Task.FromResult(new List<Fragment>());
|
||||
if (string.IsNullOrWhiteSpace(q)) return [];
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.Where(f => f.Tags?.Any(t => !string.IsNullOrWhiteSpace(t) && t.Contains(q, StringComparison.OrdinalIgnoreCase)) == true).ToList());
|
||||
return _store.Where(f => f.Tags?.Any(t => !string.IsNullOrWhiteSpace(t) && t.Contains(q, StringComparison.OrdinalIgnoreCase)) == true).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTypeAsync(string type)
|
||||
public List<Fragment> GetByType(string type)
|
||||
{
|
||||
var q = type?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q)) return Task.FromResult(new List<Fragment>());
|
||||
if (string.IsNullOrWhiteSpace(q)) return [];
|
||||
lock (_lock)
|
||||
{
|
||||
return Task.FromResult(_store.Where(f => !string.IsNullOrWhiteSpace(f.Type) && f.Type.Trim().Contains(q, StringComparison.OrdinalIgnoreCase)).ToList());
|
||||
return _store.Where(f => !string.IsNullOrWhiteSpace(f.Type) && f.Type.Trim().Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
public List<Fragment> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
{
|
||||
var results = _store.AsEnumerable();
|
||||
var qType = type?.Trim();
|
||||
@ -120,7 +119,7 @@ public class InMemoryFragmentRepository : IFragmentRepository
|
||||
if (timeAfter.HasValue)
|
||||
results = results.Where(f => f.Time > timeAfter.Value);
|
||||
|
||||
return Task.FromResult(results.ToList());
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,37 +8,33 @@ public sealed class SqliteFragmentRepository(IDatabaseSessionService session) :
|
||||
{
|
||||
private readonly IDatabaseSessionService _session = session;
|
||||
|
||||
public Task<List<Fragment>> GetAllAsync()
|
||||
public List<Fragment> GetAll()
|
||||
{
|
||||
var conn = _session.GetConnection();
|
||||
var fragments = ReadAllFragments(conn);
|
||||
return Task.FromResult(fragments);
|
||||
return ReadAllFragments(conn);
|
||||
}
|
||||
|
||||
public Task<Fragment?> GetByIdAsync(Guid id)
|
||||
public Fragment? GetById(Guid id)
|
||||
{
|
||||
var conn = _session.GetConnection();
|
||||
var fragment = ReadFragment(conn, id);
|
||||
return Task.FromResult(fragment);
|
||||
return ReadFragment(conn, id);
|
||||
}
|
||||
|
||||
public Task AddAsync(Fragment fragment)
|
||||
public void Add(Fragment fragment)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fragment);
|
||||
Normalize(fragment);
|
||||
var conn = _session.GetConnection();
|
||||
InsertFragment(conn, fragment);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(Guid id)
|
||||
public bool Remove(Guid id)
|
||||
{
|
||||
var conn = _session.GetConnection();
|
||||
var deleted = DeleteFragment(conn, id);
|
||||
return Task.FromResult(deleted);
|
||||
return DeleteFragment(conn, id);
|
||||
}
|
||||
|
||||
public Task<bool> UpdateAsync(
|
||||
public bool Update(
|
||||
Guid id,
|
||||
string? type = null,
|
||||
string? description = null,
|
||||
@ -48,7 +44,7 @@ public sealed class SqliteFragmentRepository(IDatabaseSessionService session) :
|
||||
var conn = _session.GetConnection();
|
||||
var existing = ReadFragment(conn, id);
|
||||
if (existing is null)
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
@ -75,38 +71,36 @@ public sealed class SqliteFragmentRepository(IDatabaseSessionService session) :
|
||||
existing.Time = time.Value;
|
||||
|
||||
UpdateFragmentRow(conn, existing);
|
||||
return Task.FromResult(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTagAsync(string tag)
|
||||
public List<Fragment> GetByTag(string tag)
|
||||
{
|
||||
var q = tag?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return Task.FromResult(new List<Fragment>());
|
||||
return [];
|
||||
|
||||
var conn = _session.GetConnection();
|
||||
var all = ReadAllFragments(conn);
|
||||
var items = all
|
||||
return all
|
||||
.Where(f => f.Tags.Any(t => t.Contains(q, StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> GetByTypeAsync(string type)
|
||||
public List<Fragment> GetByType(string type)
|
||||
{
|
||||
var q = type?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
return Task.FromResult(new List<Fragment>());
|
||||
return [];
|
||||
|
||||
var conn = _session.GetConnection();
|
||||
var all = ReadAllFragments(conn);
|
||||
var items = all
|
||||
return all
|
||||
.Where(f => f.Type.Contains(q, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
return Task.FromResult(items);
|
||||
}
|
||||
|
||||
public Task<List<Fragment>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
public List<Fragment> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
{
|
||||
var conn = _session.GetConnection();
|
||||
IEnumerable<Fragment> results = ReadAllFragments(conn);
|
||||
@ -121,7 +115,7 @@ public sealed class SqliteFragmentRepository(IDatabaseSessionService session) :
|
||||
if (timeAfter.HasValue)
|
||||
results = results.Where(f => f.Time > timeAfter.Value);
|
||||
|
||||
return Task.FromResult(results.ToList());
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────────
|
||||
|
||||
@ -49,8 +49,8 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
|
||||
if (_connection is not null)
|
||||
return _connection;
|
||||
|
||||
_connection = OpenEncryptedConnection(_password, _dataDirectory);
|
||||
EnsureSchema(_connection, _database);
|
||||
_connection = _database.OpenEncryptedConnection(_password, _dataDirectory);
|
||||
_database.EnsureSchema(_connection);
|
||||
return _connection;
|
||||
}
|
||||
}
|
||||
@ -63,37 +63,4 @@ public sealed class DatabaseSessionService(IJournalDatabaseService database) : I
|
||||
_connection = null;
|
||||
}
|
||||
}
|
||||
|
||||
private SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory)
|
||||
{
|
||||
var dbPath = _database.GetDatabasePath(dataDirectory);
|
||||
var directory = Path.GetDirectoryName(dbPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
var connection = new SqliteConnection(
|
||||
$"Data Source={dbPath};Mode=ReadWriteCreate;Pooling=False");
|
||||
connection.Open();
|
||||
|
||||
using var keyCmd = connection.CreateCommand();
|
||||
keyCmd.CommandText = _database.BuildPragmaKeyStatement(password) + ";";
|
||||
keyCmd.ExecuteNonQuery();
|
||||
|
||||
// Verify the key is correct
|
||||
using var verifyCmd = connection.CreateCommand();
|
||||
verifyCmd.CommandText = "SELECT count(*) FROM sqlite_master;";
|
||||
_ = verifyCmd.ExecuteScalar();
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private static void EnsureSchema(SqliteConnection connection, IJournalDatabaseService database)
|
||||
{
|
||||
foreach (var statement in database.GetSchemaStatements().Values)
|
||||
{
|
||||
using var cmd = connection.CreateCommand();
|
||||
cmd.CommandText = statement;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Journal.Core.Dtos;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Journal.Core.Services.Database;
|
||||
|
||||
@ -8,6 +9,8 @@ public interface IJournalDatabaseService
|
||||
byte[] DeriveDatabaseKey(string password);
|
||||
string BuildPragmaKeyStatement(string password);
|
||||
IReadOnlyDictionary<string, string> GetSchemaStatements();
|
||||
SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null);
|
||||
void EnsureSchema(SqliteConnection connection);
|
||||
string WriteSchemaBootstrap(string? dataDirectory = null);
|
||||
JournalDatabaseStatus GetStatus(string password, string? dataDirectory = null);
|
||||
JournalDatabaseHydrationResult HydrateWorkspace(string password, string? dataDirectory = null);
|
||||
|
||||
@ -135,7 +135,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
using var connection = OpenEncryptedConnection(password, directory);
|
||||
CreateSchema(connection);
|
||||
EnsureSchema(connection);
|
||||
var runtimeReady = HasRequiredTables(connection);
|
||||
|
||||
var entryFilesProcessed = Directory.GetFiles(directory, "*.md", SearchOption.TopDirectoryOnly).Length;
|
||||
@ -166,7 +166,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
||||
}
|
||||
}
|
||||
|
||||
private SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null)
|
||||
public SqliteConnection OpenEncryptedConnection(string password, string? dataDirectory = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(password))
|
||||
throw new ArgumentException("Password cannot be empty.", nameof(password));
|
||||
@ -187,7 +187,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
||||
return connection;
|
||||
}
|
||||
|
||||
private void CreateSchema(SqliteConnection connection)
|
||||
public void EnsureSchema(SqliteConnection connection)
|
||||
{
|
||||
foreach (var statement in GetSchemaStatements().Values)
|
||||
{
|
||||
@ -217,7 +217,7 @@ public sealed class JournalDatabaseService(IJournalConfigService config) : IJour
|
||||
try
|
||||
{
|
||||
using var connection = OpenEncryptedConnection(password, dataDirectory);
|
||||
CreateSchema(connection);
|
||||
EnsureSchema(connection);
|
||||
var ready = HasRequiredTables(connection);
|
||||
return ready
|
||||
? (true, "SQLCipher runtime is available and schema tables are present.")
|
||||
|
||||
@ -17,7 +17,7 @@ public class FragmentService(IFragmentRepository repo) : IFragmentService
|
||||
f.Tags != null ? [.. f.Tags] : []
|
||||
);
|
||||
|
||||
public async Task<FragmentDto> CreateAsync(CreateFragmentDto dto)
|
||||
public FragmentDto Create(CreateFragmentDto dto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
|
||||
@ -28,11 +28,11 @@ public class FragmentService(IFragmentRepository repo) : IFragmentService
|
||||
if (dto.Tags != null)
|
||||
f.Tags = [.. dto.Tags.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t!.Trim())];
|
||||
|
||||
await _repo.AddAsync(f);
|
||||
_repo.Add(f);
|
||||
return Map(f);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateAsync(Guid id, UpdateFragmentDto dto)
|
||||
public bool Update(Guid id, UpdateFragmentDto dto)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dto);
|
||||
if (dto.Type != null && string.IsNullOrWhiteSpace(dto.Type))
|
||||
@ -44,38 +44,38 @@ public class FragmentService(IFragmentRepository repo) : IFragmentService
|
||||
var description = dto.Description?.Trim();
|
||||
var tags = dto.Tags?.Where(t => !string.IsNullOrWhiteSpace(t)).Select(t => t!.Trim()).ToList();
|
||||
|
||||
return await _repo.UpdateAsync(id, type, description, tags, dto.Time);
|
||||
return _repo.Update(id, type, description, tags, dto.Time);
|
||||
}
|
||||
|
||||
public Task<bool> RemoveAsync(Guid id) => _repo.RemoveAsync(id);
|
||||
public bool Remove(Guid id) => _repo.Remove(id);
|
||||
|
||||
public async Task<List<FragmentDto>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
public List<FragmentDto> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null)
|
||||
{
|
||||
var items = await _repo.SearchAsync(type, tag, timeAfter);
|
||||
var items = _repo.Search(type, tag, timeAfter);
|
||||
return [.. items.Select(Map)];
|
||||
}
|
||||
|
||||
public async Task<List<FragmentDto>> GetByTagAsync(string tag)
|
||||
public List<FragmentDto> GetByTag(string tag)
|
||||
{
|
||||
var items = await _repo.GetByTagAsync(tag);
|
||||
var items = _repo.GetByTag(tag);
|
||||
return [.. items.Select(Map)];
|
||||
}
|
||||
|
||||
public async Task<List<FragmentDto>> GetByTypeAsync(string type)
|
||||
public List<FragmentDto> GetByType(string type)
|
||||
{
|
||||
var items = await _repo.GetByTypeAsync(type);
|
||||
var items = _repo.GetByType(type);
|
||||
return [.. items.Select(Map)];
|
||||
}
|
||||
|
||||
public async Task<List<FragmentDto>> GetAllAsync()
|
||||
public List<FragmentDto> GetAll()
|
||||
{
|
||||
var items = await _repo.GetAllAsync();
|
||||
var items = _repo.GetAll();
|
||||
return [.. items.Select(Map)];
|
||||
}
|
||||
|
||||
public async Task<FragmentDto?> GetByIdAsync(Guid id)
|
||||
public FragmentDto? GetById(Guid id)
|
||||
{
|
||||
var f = await _repo.GetByIdAsync(id);
|
||||
var f = _repo.GetById(id);
|
||||
return f is null ? null : Map(f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,12 +4,12 @@ namespace Journal.Core.Services.Fragments;
|
||||
|
||||
public interface IFragmentService
|
||||
{
|
||||
Task<FragmentDto> CreateAsync(CreateFragmentDto dto);
|
||||
Task<bool> UpdateAsync(Guid id, UpdateFragmentDto dto);
|
||||
Task<bool> RemoveAsync(Guid id);
|
||||
Task<List<FragmentDto>> SearchAsync(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null);
|
||||
Task<List<FragmentDto>> GetByTagAsync(string tag);
|
||||
Task<List<FragmentDto>> GetByTypeAsync(string type);
|
||||
Task<List<FragmentDto>> GetAllAsync();
|
||||
Task<FragmentDto?> GetByIdAsync(Guid id);
|
||||
FragmentDto Create(CreateFragmentDto dto);
|
||||
bool Update(Guid id, UpdateFragmentDto dto);
|
||||
bool Remove(Guid id);
|
||||
List<FragmentDto> Search(string? type = null, string? tag = null, DateTimeOffset? timeAfter = null);
|
||||
List<FragmentDto> GetByTag(string tag);
|
||||
List<FragmentDto> GetByType(string type);
|
||||
List<FragmentDto> GetAll();
|
||||
FragmentDto? GetById(Guid id);
|
||||
}
|
||||
|
||||
@ -133,49 +133,51 @@ static Entry NewEntry()
|
||||
|
||||
static IJournalDatabaseService NewDatabaseService() => new JournalDatabaseService(new JournalConfigService());
|
||||
|
||||
static async Task TestCreateTrimsAsync()
|
||||
static Task TestCreateTrimsAsync()
|
||||
{
|
||||
var service = NewService();
|
||||
var created = await service.CreateAsync(new CreateFragmentDto(" !TRIGGER ", " stomach drop ", [" stress ", "", " body "]));
|
||||
var created = service.Create(new CreateFragmentDto(" !TRIGGER ", " stomach drop ", [" stress ", "", " body "]));
|
||||
|
||||
Assert(created.Type == "!TRIGGER", "Type should be trimmed.");
|
||||
Assert(created.Description == "stomach drop", "Description should be trimmed.");
|
||||
Assert(created.Tags.Count == 2, "Expected two normalized tags.");
|
||||
Assert(created.Tags[0] == "stress" && created.Tags[1] == "body", "Tags should be trimmed and preserved.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static async Task TestUpdateAcceptsTypeAsync()
|
||||
static Task TestUpdateAcceptsTypeAsync()
|
||||
{
|
||||
var service = NewService();
|
||||
var created = await service.CreateAsync(new CreateFragmentDto("!TRIGGER", "one"));
|
||||
var ok = await service.UpdateAsync(created.Id, new UpdateFragmentDto(Type: " !FLASHBACK ", Description: " two ", Tags: [" memory "]));
|
||||
var created = service.Create(new CreateFragmentDto("!TRIGGER", "one"));
|
||||
var ok = service.Update(created.Id, new UpdateFragmentDto(Type: " !FLASHBACK ", Description: " two ", Tags: [" memory "]));
|
||||
|
||||
Assert(ok, "Expected update to succeed.");
|
||||
var updated = await service.GetByIdAsync(created.Id);
|
||||
var updated = service.GetById(created.Id);
|
||||
Assert(updated is not null, "Updated fragment should exist.");
|
||||
Assert(updated!.Type == "!FLASHBACK", "Updated type should be trimmed and stored.");
|
||||
Assert(updated.Description == "two", "Updated description should be trimmed and stored.");
|
||||
Assert(updated.Tags.Count == 1 && updated.Tags[0] == "memory", "Updated tags should be normalized.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static async Task TestUpdateRejectsWhitespaceTypeAsync()
|
||||
static Task TestUpdateRejectsWhitespaceTypeAsync()
|
||||
{
|
||||
var service = NewService();
|
||||
var created = await service.CreateAsync(new CreateFragmentDto("!TRIGGER", "desc"));
|
||||
var created = service.Create(new CreateFragmentDto("!TRIGGER", "desc"));
|
||||
|
||||
try
|
||||
{
|
||||
_ = await service.UpdateAsync(created.Id, new UpdateFragmentDto(Type: " "));
|
||||
_ = service.Update(created.Id, new UpdateFragmentDto(Type: " "));
|
||||
}
|
||||
catch (ValidationException)
|
||||
{
|
||||
return;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Expected ValidationException for whitespace type update.");
|
||||
}
|
||||
|
||||
static async Task TestFileRepositoryPersistsAsync()
|
||||
static Task TestFileRepositoryPersistsAsync()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "journal-smoke", Guid.NewGuid().ToString("N"));
|
||||
var dataDir = Path.Combine(tempRoot, "data");
|
||||
@ -193,14 +195,14 @@ static async Task TestFileRepositoryPersistsAsync()
|
||||
session1.SetPassword(password, dataDir);
|
||||
var repo1 = new SqliteFragmentRepository(session1);
|
||||
var service1 = new FragmentService(repo1);
|
||||
var created = await service1.CreateAsync(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"]));
|
||||
var created = service1.Create(new CreateFragmentDto("!TRIGGER", "persist me", ["tag1"]));
|
||||
|
||||
// Second session: verify persistence
|
||||
using var session2 = new DatabaseSessionService(dbService);
|
||||
session2.SetPassword(password, dataDir);
|
||||
var repo2 = new SqliteFragmentRepository(session2);
|
||||
var service2 = new FragmentService(repo2);
|
||||
var loaded = await service2.GetByIdAsync(created.Id);
|
||||
var loaded = service2.GetById(created.Id);
|
||||
|
||||
Assert(loaded is not null, "Expected fragment to persist across repository instances.");
|
||||
Assert(loaded!.Description == "persist me", "Persisted fragment description mismatch.");
|
||||
@ -211,6 +213,8 @@ static async Task TestFileRepositoryPersistsAsync()
|
||||
if (Directory.Exists(tempRoot))
|
||||
Directory.Delete(tempRoot, recursive: true);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
static Task TestJournalEntryModelAsync()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user