- Add Rust shutdown command that kills sidecar and exits app cleanly
- Frontend calls invoke('shutdown') after vault flush on close
- Add auth.ts, entries.ts, and normalize.ts backend modules
- Add EditorPanel.svelte component
- Expand entries store with full CRUD support
- Add JournalEntryDtos and JournalEntryDtoMapper in Journal.Core
- Update entry search, fragments, and sidecar CLI
Co-Authored-By: Oz <oz-agent@warp.dev>
109 lines
4.5 KiB
C#
109 lines
4.5 KiB
C#
using Journal.Core.Dtos;
|
|
using System.Globalization;
|
|
|
|
namespace Journal.Core.Services.Entries;
|
|
|
|
public class EntrySearchService : IEntrySearchService
|
|
{
|
|
public Task<IReadOnlyList<EntrySearchResultDto>> SearchEntriesAsync(EntrySearchRequestDto request)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(request);
|
|
if (string.IsNullOrWhiteSpace(request.DataDirectory))
|
|
throw new ArgumentException("Data directory is required.", nameof(request.DataDirectory));
|
|
|
|
if (!Directory.Exists(request.DataDirectory))
|
|
return Task.FromResult<IReadOnlyList<EntrySearchResultDto>>([]);
|
|
|
|
var hasQuery = !string.IsNullOrWhiteSpace(request.Query);
|
|
var query = request.Query?.Trim() ?? "";
|
|
var hasSectionFilter = !string.IsNullOrWhiteSpace(request.Section);
|
|
var section = request.Section?.Trim() ?? "";
|
|
|
|
var typeSet = NormalizeSet(request.Types);
|
|
var tagSet = NormalizeSet(request.Tags);
|
|
var checkedSet = NormalizeSet(request.Checked);
|
|
var uncheckedSet = NormalizeSet(request.Unchecked);
|
|
var hasFragmentFilters = typeSet.Count > 0 || tagSet.Count > 0;
|
|
var hasCheckboxFilters = checkedSet.Count > 0 || uncheckedSet.Count > 0;
|
|
|
|
var startDate = ParseOptionalDate(request.StartDate, nameof(request.StartDate));
|
|
var endDate = ParseOptionalDate(request.EndDate, nameof(request.EndDate));
|
|
if (startDate.HasValue && endDate.HasValue && startDate.Value > endDate.Value)
|
|
throw new ArgumentException("startDate cannot be after endDate.");
|
|
|
|
var results = new List<EntrySearchResultDto>();
|
|
foreach (var filePath in Directory.GetFiles(request.DataDirectory, "*.md")
|
|
.OrderBy(Path.GetFileName, StringComparer.Ordinal))
|
|
{
|
|
var fileName = Path.GetFileName(filePath);
|
|
var fileStem = Path.GetFileNameWithoutExtension(filePath);
|
|
var rawContent = File.ReadAllText(filePath);
|
|
var entry = JournalParser.ParseJournalContent(rawContent, fileStem);
|
|
|
|
if (startDate.HasValue || endDate.HasValue)
|
|
{
|
|
if (!DateOnly.TryParseExact(entry.Date, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var entryDate))
|
|
continue;
|
|
|
|
if (startDate.HasValue && entryDate < startDate.Value)
|
|
continue;
|
|
if (endDate.HasValue && entryDate > endDate.Value)
|
|
continue;
|
|
}
|
|
|
|
var contentMatch = true;
|
|
if (hasQuery)
|
|
{
|
|
var haystack = hasSectionFilter ? entry.GetSection(section) : entry.RawContent;
|
|
contentMatch = haystack.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
if (!contentMatch)
|
|
continue;
|
|
|
|
var fragmentMatch = !hasFragmentFilters || entry.Fragments.Any(fragment =>
|
|
(typeSet.Count == 0 || typeSet.Contains(fragment.Type)) &&
|
|
(tagSet.Count == 0 || fragment.Tags.Any(tagSet.Contains)));
|
|
if (!fragmentMatch)
|
|
continue;
|
|
|
|
var checkboxMatch = !hasCheckboxFilters || entry.Sections.Values.Any(sectionValue =>
|
|
sectionValue.Checkboxes.Any(checkbox =>
|
|
(checkedSet.Count > 0 && checkbox.Value && checkedSet.Contains(checkbox.Key)) ||
|
|
(uncheckedSet.Count > 0 && !checkbox.Value && uncheckedSet.Contains(checkbox.Key))));
|
|
if (!checkboxMatch)
|
|
continue;
|
|
|
|
results.Add(new EntrySearchResultDto(fileName, entry.ToDto()));
|
|
}
|
|
|
|
return Task.FromResult<IReadOnlyList<EntrySearchResultDto>>(results);
|
|
}
|
|
|
|
private static HashSet<string> NormalizeSet(IReadOnlyList<string>? values)
|
|
{
|
|
if (values is null || values.Count == 0)
|
|
return [];
|
|
|
|
var set = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var value in values)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
continue;
|
|
set.Add(value.Trim());
|
|
}
|
|
|
|
return set;
|
|
}
|
|
|
|
private static DateOnly? ParseOptionalDate(string? raw, string argumentName)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(raw))
|
|
return null;
|
|
|
|
if (DateOnly.TryParseExact(raw.Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date))
|
|
return date;
|
|
|
|
throw new ArgumentException($"Invalid {argumentName} value. Expected yyyy-MM-dd.");
|
|
}
|
|
}
|