< Summary

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

#LineLine coverage
 1using Chronicis.Api.Data;
 2using Chronicis.Api.Models;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Enums;
 5using Microsoft.EntityFrameworkCore;
 6
 7namespace Chronicis.Api.Services;
 8
 9public sealed class WorldLinkSuggestionService : IWorldLinkSuggestionService
 10{
 11    private readonly ChronicisDbContext _context;
 12    private readonly IArticleHierarchyService _hierarchyService;
 13
 14    public WorldLinkSuggestionService(ChronicisDbContext context, IArticleHierarchyService hierarchyService)
 15    {
 216        _context = context;
 217        _hierarchyService = hierarchyService;
 218    }
 19
 20    public async Task<ServiceResult<List<LinkSuggestionDto>>> GetSuggestionsAsync(Guid worldId, string query, Guid userI
 21    {
 22        var hasAccess = await _context.WorldMembers
 23            .AnyAsync(wm => wm.WorldId == worldId && wm.UserId == userId);
 24
 25        if (!hasAccess)
 26        {
 27            return ServiceResult<List<LinkSuggestionDto>>.Forbidden();
 28        }
 29
 30        var normalizedQuery = query.ToLowerInvariant();
 31
 32        var titleMatches = await _context.Articles
 33            .Where(a => a.WorldId == worldId)
 34            .Where(a => a.Type != ArticleType.Tutorial && a.WorldId != Guid.Empty)
 35            .Where(a => a.Title != null && a.Title.ToLower().Contains(normalizedQuery))
 36            .OrderBy(a => a.Title)
 37            .Take(20)
 38            .Select(a => new LinkSuggestionDto
 39            {
 40                ArticleId = a.Id,
 41                Title = a.Title ?? "Untitled",
 42                Slug = a.Slug,
 43                ArticleType = a.Type,
 44                DisplayPath = string.Empty,
 45                MatchedAlias = null
 46            })
 47            .ToListAsync();
 48
 49        var titleMatchIds = titleMatches.Select(t => t.ArticleId).ToHashSet();
 50
 51        var aliasMatches = await _context.ArticleAliases
 52            .Include(aa => aa.Article)
 53            .Where(aa => aa.Article.WorldId == worldId)
 54            .Where(aa => aa.Article.Type != ArticleType.Tutorial && aa.Article.WorldId != Guid.Empty)
 55            .Where(aa => aa.AliasText.ToLower().Contains(normalizedQuery))
 56            .Where(aa => !titleMatchIds.Contains(aa.ArticleId))
 57            .OrderBy(aa => aa.AliasText)
 58            .Take(20)
 59            .Select(aa => new LinkSuggestionDto
 60            {
 61                ArticleId = aa.ArticleId,
 62                Title = aa.Article.Title ?? "Untitled",
 63                Slug = aa.Article.Slug,
 64                ArticleType = aa.Article.Type,
 65                DisplayPath = string.Empty,
 66                MatchedAlias = aa.AliasText
 67            })
 68            .ToListAsync();
 69
 70        var suggestions = titleMatches
 71            .Concat(aliasMatches)
 72            .Take(20)
 73            .ToList();
 74
 75        // Batch breadcrumb lookup: one set of O(depth) queries for all suggestions
 76        // instead of O(suggestions × depth) serial calls to BuildDisplayPathAsync.
 77        var displayPathOptions = new HierarchyWalkOptions
 78        {
 79            PublicOnly = false,
 80            IncludeWorldBreadcrumb = false,
 81            IncludeVirtualGroups = false,
 82            IncludeCurrentArticle = true
 83        };
 84
 85        var ancestorPaths = await _hierarchyService.BuildBreadcrumbsBatchAsync(
 86            suggestions.Select(s => s.ArticleId),
 87            displayPathOptions);
 88
 89        foreach (var suggestion in suggestions)
 90        {
 91            if (ancestorPaths.TryGetValue(suggestion.ArticleId, out var breadcrumbs))
 92            {
 93                var titles = breadcrumbs.Select(b => b.Title).ToList();
 94                if (titles.Count > 1)
 95                    titles.RemoveAt(0); // strip top-level root (mirrors BuildDisplayPathAsync default)
 96                suggestion.DisplayPath = string.Join(" / ", titles);
 97            }
 98        }
 99
 100        return ServiceResult<List<LinkSuggestionDto>>.Success(suggestions);
 101    }
 102}
 103