< Summary

Information
Class: Chronicis.Api.Services.ArticleDataAccessService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/ArticleDataAccessService.cs
Line coverage
100%
Covered lines: 6
Uncovered lines: 0
Coverable lines: 6
Total lines: 230
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%
SaveChangesAsync()100%11100%
GetReadableArticlesQuery(...)100%11100%

File(s)

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Chronicis.Shared.Models;
 5using Chronicis.Shared.Utilities;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace Chronicis.Api.Services;
 9
 10public sealed class ArticleDataAccessService : IArticleDataAccessService
 11{
 12    private readonly ChronicisDbContext _context;
 13    private readonly IWorldDocumentService _worldDocumentService;
 14    private readonly IReadAccessPolicyService _readAccessPolicy;
 15
 16    public ArticleDataAccessService(
 17        ChronicisDbContext context,
 18        IWorldDocumentService worldDocumentService,
 19        IReadAccessPolicyService readAccessPolicy)
 20    {
 821        _context = context;
 822        _worldDocumentService = worldDocumentService;
 823        _readAccessPolicy = readAccessPolicy;
 824    }
 25
 26    public async Task AddArticleAsync(Article article)
 27    {
 28        _context.Articles.Add(article);
 29        await _context.SaveChangesAsync();
 30    }
 31
 132    public Task SaveChangesAsync() => _context.SaveChangesAsync();
 33
 34    public async Task<Article?> FindReadableArticleAsync(Guid articleId, Guid userId)
 35    {
 36        return await GetReadableArticlesQuery(userId)
 37            .Where(a => a.Id == articleId)
 38            .FirstOrDefaultAsync();
 39    }
 40
 41    public async Task<bool> IsTutorialSlugUniqueAsync(string slug, Guid? parentId, Guid? excludeArticleId = null)
 42    {
 43        var query = _context.Articles
 44            .AsNoTracking()
 45            .Where(a => a.Type == ArticleType.Tutorial && a.WorldId == Guid.Empty && a.Slug == slug);
 46
 47        query = parentId.HasValue
 48            ? query.Where(a => a.ParentId == parentId.Value)
 49            : query.Where(a => a.ParentId == null);
 50
 51        if (excludeArticleId.HasValue)
 52        {
 53            query = query.Where(a => a.Id != excludeArticleId.Value);
 54        }
 55
 56        return !await query.AnyAsync();
 57    }
 58
 59    public async Task<string> GenerateTutorialSlugAsync(string title, Guid? parentId, Guid? excludeArticleId = null)
 60    {
 61        var baseSlug = SlugGenerator.GenerateSlug(title);
 62
 63        var query = _context.Articles
 64            .AsNoTracking()
 65            .Where(a => a.Type == ArticleType.Tutorial && a.WorldId == Guid.Empty);
 66
 67        query = parentId.HasValue
 68            ? query.Where(a => a.ParentId == parentId.Value)
 69            : query.Where(a => a.ParentId == null);
 70
 71        if (excludeArticleId.HasValue)
 72        {
 73            query = query.Where(a => a.Id != excludeArticleId.Value);
 74        }
 75
 76        var existingSlugs = await query
 77            .Select(a => a.Slug)
 78            .ToHashSetAsync();
 79
 80        return SlugGenerator.GenerateUniqueSlug(baseSlug, existingSlugs);
 81    }
 82
 83    public async Task DeleteArticleAndDescendantsAsync(Guid articleId)
 84    {
 85        var children = await _context.Articles
 86            .Where(a => a.ParentId == articleId)
 87            .Select(a => a.Id)
 88            .ToListAsync();
 89
 90        foreach (var childId in children)
 91        {
 92            await DeleteArticleAndDescendantsAsync(childId);
 93        }
 94
 95        var linksToDelete = await _context.ArticleLinks
 96            .Where(l => l.SourceArticleId == articleId || l.TargetArticleId == articleId)
 97            .ToListAsync();
 98        _context.ArticleLinks.RemoveRange(linksToDelete);
 99
 100        await _worldDocumentService.DeleteArticleImagesAsync(articleId);
 101
 102        var article = await _context.Articles.FindAsync(articleId);
 103        if (article != null)
 104        {
 105            _context.Articles.Remove(article);
 106        }
 107
 108        await _context.SaveChangesAsync();
 109    }
 110
 111    public async Task<List<BacklinkDto>> GetBacklinksAsync(Guid articleId)
 112    {
 113        return await _context.ArticleLinks
 114            .Where(l => l.TargetArticleId == articleId)
 115            .Select(l => new BacklinkDto
 116            {
 117                ArticleId = l.SourceArticleId,
 118                Title = l.SourceArticle.Title ?? "Untitled",
 119                Slug = l.SourceArticle.Slug,
 120                Snippet = l.DisplayText,
 121                DisplayPath = string.Empty
 122            })
 123            .Distinct()
 124            .ToListAsync();
 125    }
 126
 127    public async Task<List<BacklinkDto>> GetOutgoingLinksAsync(Guid articleId)
 128    {
 129        return await _context.ArticleLinks
 130            .Where(l => l.SourceArticleId == articleId)
 131            .Select(l => new BacklinkDto
 132            {
 133                ArticleId = l.TargetArticleId,
 134                Title = l.TargetArticle.Title ?? "Untitled",
 135                Slug = l.TargetArticle.Slug,
 136                Snippet = l.DisplayText,
 137                DisplayPath = string.Empty
 138            })
 139            .Distinct()
 140            .ToListAsync();
 141    }
 142
 143    public async Task<List<ResolvedLinkDto>> ResolveReadableLinksAsync(IEnumerable<Guid> articleIds, Guid userId)
 144    {
 145        var requested = articleIds.ToList();
 146        return await GetReadableArticlesQuery(userId)
 147            .Where(a => requested.Contains(a.Id))
 148            .Select(a => new ResolvedLinkDto
 149            {
 150                ArticleId = a.Id,
 151                Exists = true,
 152                Title = a.Title,
 153                Slug = a.Slug
 154            })
 155            .ToListAsync();
 156    }
 157
 158    public async Task<(bool Found, Guid? WorldId)> TryGetReadableArticleWorldAsync(Guid articleId, Guid userId)
 159    {
 160        var article = await GetReadableArticlesQuery(userId)
 161            .Where(a => a.Id == articleId)
 162            .Select(a => new { a.WorldId })
 163            .FirstOrDefaultAsync();
 164
 165        return article == null
 166            ? (false, null)
 167            : (true, article.WorldId);
 168    }
 169
 170    public async Task<Article?> GetReadableArticleWithAliasesAsync(Guid articleId, Guid userId)
 171    {
 172        var worldReadable = await _readAccessPolicy
 173            .ApplyAuthenticatedWorldArticleFilter(_context.Articles, userId)
 174            .Include(a => a.Aliases)
 175            .Where(a => a.Id == articleId)
 176            .FirstOrDefaultAsync();
 177
 178        if (worldReadable != null)
 179        {
 180            return worldReadable;
 181        }
 182
 183        return await _readAccessPolicy
 184            .ApplyTutorialArticleFilter(_context.Articles)
 185            .Include(a => a.Aliases)
 186            .Where(a => a.Id == articleId)
 187            .FirstOrDefaultAsync();
 188    }
 189
 190    public async Task UpsertAliasesAsync(Article article, IReadOnlyCollection<string> newAliases, Guid userId)
 191    {
 192        var aliasesToRemove = article.Aliases
 193            .Where(existing => !newAliases.Contains(existing.AliasText, StringComparer.OrdinalIgnoreCase))
 194            .ToList();
 195
 196        foreach (var alias in aliasesToRemove)
 197        {
 198            _context.ArticleAliases.Remove(alias);
 199        }
 200
 201        var existingAliasTexts = article.Aliases
 202            .Select(a => a.AliasText.ToLowerInvariant())
 203            .ToHashSet();
 204
 205        foreach (var aliasText in newAliases)
 206        {
 207            if (!existingAliasTexts.Contains(aliasText.ToLowerInvariant()))
 208            {
 209                _context.ArticleAliases.Add(new ArticleAlias
 210                {
 211                    Id = Guid.NewGuid(),
 212                    ArticleId = article.Id,
 213                    AliasText = aliasText,
 214                    CreatedAt = DateTime.UtcNow
 215                });
 216            }
 217        }
 218
 219        article.ModifiedAt = DateTime.UtcNow;
 220        article.LastModifiedBy = userId;
 221
 222        await _context.SaveChangesAsync();
 223    }
 224
 225    private IQueryable<Article> GetReadableArticlesQuery(Guid userId)
 226    {
 10227        return _readAccessPolicy.ApplyAuthenticatedReadableArticleFilter(_context.Articles, userId);
 228    }
 229}
 230