| | | 1 | | using Chronicis.Api.Data; |
| | | 2 | | using Chronicis.Shared.Models; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | |
| | | 5 | | namespace Chronicis.Api.Services; |
| | | 6 | | |
| | | 7 | | /// <summary> |
| | | 8 | | /// Synchronizes wiki links in the database based on article content. |
| | | 9 | | /// Uses a delete-then-insert strategy for simplicity. |
| | | 10 | | /// </summary> |
| | | 11 | | public class LinkSyncService : ILinkSyncService |
| | | 12 | | { |
| | | 13 | | private readonly ChronicisDbContext _context; |
| | | 14 | | private readonly ILinkParser _linkParser; |
| | | 15 | | private readonly ILogger<LinkSyncService> _logger; |
| | | 16 | | |
| | 12 | 17 | | public LinkSyncService( |
| | 12 | 18 | | ChronicisDbContext context, |
| | 12 | 19 | | ILinkParser linkParser, |
| | 12 | 20 | | ILogger<LinkSyncService> logger) |
| | | 21 | | { |
| | 12 | 22 | | _context = context; |
| | 12 | 23 | | _linkParser = linkParser; |
| | 12 | 24 | | _logger = logger; |
| | 12 | 25 | | } |
| | | 26 | | |
| | | 27 | | /// <summary> |
| | | 28 | | /// Synchronizes the ArticleLink table for the given article. |
| | | 29 | | /// Removes all existing links for this article and creates new ones based on the body content. |
| | | 30 | | /// </summary> |
| | | 31 | | /// <param name="sourceArticleId">The ID of the article whose links should be synced.</param> |
| | | 32 | | /// <param name="body">The article body containing wiki links to parse.</param> |
| | | 33 | | public async Task SyncLinksAsync(Guid sourceArticleId, string? body) |
| | | 34 | | { |
| | | 35 | | // Step 1: Delete all existing links where this article is the source |
| | 14 | 36 | | var existingLinks = await _context.ArticleLinks |
| | 14 | 37 | | .Where(al => al.SourceArticleId == sourceArticleId) |
| | 14 | 38 | | .ToListAsync(); |
| | | 39 | | |
| | 14 | 40 | | var removedCount = existingLinks.Count; |
| | | 41 | | |
| | 14 | 42 | | if (existingLinks.Any()) |
| | | 43 | | { |
| | 3 | 44 | | _context.ArticleLinks.RemoveRange(existingLinks); |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | // Step 2: Parse new links from body |
| | 14 | 48 | | var parsedLinks = _linkParser.ParseLinks(body); |
| | | 49 | | |
| | | 50 | | // Step 3: Create new ArticleLink entities |
| | 27 | 51 | | var newLinks = parsedLinks.Select(pl => new ArticleLink |
| | 27 | 52 | | { |
| | 27 | 53 | | Id = Guid.NewGuid(), |
| | 27 | 54 | | SourceArticleId = sourceArticleId, |
| | 27 | 55 | | TargetArticleId = pl.TargetArticleId, |
| | 27 | 56 | | DisplayText = pl.DisplayText, |
| | 27 | 57 | | Position = pl.Position, |
| | 27 | 58 | | CreatedAt = DateTime.UtcNow |
| | 27 | 59 | | }).ToList(); |
| | | 60 | | |
| | 14 | 61 | | var addedCount = newLinks.Count; |
| | | 62 | | |
| | 14 | 63 | | if (newLinks.Any()) |
| | | 64 | | { |
| | 10 | 65 | | await _context.ArticleLinks.AddRangeAsync(newLinks); |
| | | 66 | | } |
| | | 67 | | |
| | | 68 | | // Step 4: Save all changes in a single transaction |
| | 14 | 69 | | await _context.SaveChangesAsync(); |
| | | 70 | | |
| | | 71 | | // Step 5: Log metrics |
| | 14 | 72 | | _logger.LogDebug( |
| | 14 | 73 | | "Synced links for article {ArticleId}: {RemovedCount} removed, {AddedCount} added", |
| | 14 | 74 | | sourceArticleId, |
| | 14 | 75 | | removedCount, |
| | 14 | 76 | | addedCount); |
| | 14 | 77 | | } |
| | | 78 | | } |