< Summary

Information
Class: Chronicis.Api.Services.ArticleRenameCascadeService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ArticleRenameCascadeService.cs
Line coverage
100%
Covered lines: 4
Uncovered lines: 0
Coverable lines: 4
Total lines: 99
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ArticleRenameCascadeService.cs

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.Models;
 3using Microsoft.EntityFrameworkCore;
 4
 5namespace Chronicis.Api.Services;
 6
 7/// <summary>
 8/// Propagates article title renames by rewriting wiki-link span inner text across all
 9/// back-linked articles and by recording the previous title as an <see cref="ArticleAlias"/>.
 10///
 11/// <para>
 12/// Transaction note: the title save in <c>ArticlesController.UpdateArticle</c> commits
 13/// before this service runs. If this cascade throws, the rename is already persisted and
 14/// back-link text will be stale until a manual retry. This is accepted POC behaviour.
 15/// </para>
 16/// </summary>
 17public sealed class ArticleRenameCascadeService : IArticleRenameCascadeService
 18{
 19    private readonly ChronicisDbContext _db;
 20    private readonly IWikiLinkTitleRewriter _rewriter;
 21    private readonly ILogger<ArticleRenameCascadeService> _logger;
 22
 23    public ArticleRenameCascadeService(
 24        ChronicisDbContext db,
 25        IWikiLinkTitleRewriter rewriter,
 26        ILogger<ArticleRenameCascadeService> logger)
 27    {
 1428        _db = db;
 1429        _rewriter = rewriter;
 1430        _logger = logger;
 1431    }
 32
 33    /// <inheritdoc/>
 34    public async Task CascadeTitleChangeAsync(
 35        Guid renamedArticleId,
 36        string oldTitle,
 37        string newTitle,
 38        CancellationToken cancellationToken = default)
 39    {
 40        // Case-insensitive no-op guard
 41        if (string.Equals(oldTitle, newTitle, StringComparison.OrdinalIgnoreCase))
 42            return;
 43
 44        if (string.IsNullOrWhiteSpace(newTitle))
 45            throw new ArgumentException("New title cannot be whitespace.", nameof(newTitle));
 46
 47        // Collect source article IDs where DisplayText is null (user accepted default title)
 48        var sourceIds = await _db.ArticleLinks
 49            .Where(al => al.TargetArticleId == renamedArticleId && al.DisplayText == null)
 50            .Select(al => al.SourceArticleId)
 51            .Distinct()
 52            .ToListAsync(cancellationToken);
 53
 54        var sourceArticles = await _db.Articles
 55            .Where(a => sourceIds.Contains(a.Id))
 56            .ToListAsync(cancellationToken);
 57
 58        var rewriteCount = 0;
 59        foreach (var article in sourceArticles)
 60        {
 61            var (newBody, changed) = _rewriter.Rewrite(article.Body, renamedArticleId, newTitle);
 62            if (changed)
 63            {
 64                article.Body = newBody;
 65                // Intentionally NOT updating ModifiedAt / LastModifiedBy per spec.
 66                rewriteCount++;
 67            }
 68        }
 69
 70        // Append alias for old title on the renamed article (case-insensitive dedup)
 71        var aliasAdded = false;
 72        if (!string.IsNullOrWhiteSpace(oldTitle))
 73        {
 74            var normalised = oldTitle.ToUpperInvariant();
 75            var exists = await _db.ArticleAliases
 76                .AnyAsync(a => a.ArticleId == renamedArticleId &&
 77                               a.AliasText.ToUpper() == normalised,
 78                          cancellationToken);
 79
 80            if (!exists)
 81            {
 82                _db.ArticleAliases.Add(new ArticleAlias
 83                {
 84                    Id = Guid.NewGuid(),
 85                    ArticleId = renamedArticleId,
 86                    AliasText = oldTitle,
 87                    CreatedAt = DateTime.UtcNow,
 88                });
 89                aliasAdded = true;
 90            }
 91        }
 92
 93        await _db.SaveChangesAsync(cancellationToken);
 94
 95        _logger.LogTraceSanitized(
 96            "Cascade rename {ArticleId} '{Old}' -> '{New}' touched {Count} source article(s), alias added: {AliasAdded}"
 97            renamedArticleId, oldTitle, newTitle, rewriteCount, aliasAdded);
 98    }
 99}