Die KI kann im Text `tier:<8hex>` / `kontakt:` / `wurf:` / `gehege:` schreiben; Frontend löst per POST /refs/resolve (Präfix-Match, DB-agnostisch) zu Name + internem Link auf. Reine KI-Schreibhilfe — kein Nutzer-Button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
108 lines
4.9 KiB
C#
108 lines
4.9 KiB
C#
using GerbilManagerWebAPI.Dtos;
|
|
using Microsoft.AspNetCore.Http.HttpResults;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace GerbilManagerWebAPI.Endpoints
|
|
{
|
|
/// <summary>
|
|
/// REFS: Auflöser für getippte Kurz-Verweise (Shortlinks, git-Stil) in Freitexten.
|
|
/// Tokens im Text: <c>tier:<code></c>, <c>kontakt:<code></c>,
|
|
/// <c>wurf:<code></c>, <c>gehege:<code></c> — <c><code></c> ist ein
|
|
/// GUID-Präfix (mind. 6 Hex) oder die volle GUID. Das Frontend rendert daraus
|
|
/// Name + interner Link.
|
|
///
|
|
/// POST /refs/resolve -> [{ type, code, id|null, name|null }]
|
|
///
|
|
/// DB-AGNOSTISCH (Tests: SQLite in-memory, Prod: Postgres): pro Typ eine schlanke
|
|
/// {Id,Name}-Projektion laden und im Speicher per Präfix matchen — KEIN ::text-Cast
|
|
/// o.ä. Mehrdeutiges Präfix (mehrere Treffer) => id=null (nicht raten).
|
|
/// </summary>
|
|
public static class RefsEndpoints
|
|
{
|
|
/// <summary>Schlanke {Id,Name}-Projektion für den In-Memory-Präfix-Match.</summary>
|
|
private readonly record struct NamedRow(Guid Id, string Name);
|
|
|
|
public static IEndpointRouteBuilder MapRefsEndpoints(this IEndpointRouteBuilder app)
|
|
{
|
|
var group = app.MapGroup("/refs").WithTags("Refs");
|
|
|
|
group.MapPost("/resolve", async Task<Ok<List<RefResolution>>> (
|
|
ResolveRefsInput input, ApplicationContext db) =>
|
|
{
|
|
var refs = input.Refs ?? new List<RefRequest>();
|
|
|
|
// Welche Entitätstypen werden überhaupt angefragt? Nur diese Tabellen laden.
|
|
var wantedTypes = refs
|
|
.Select(r => NormalizeType(r.Type))
|
|
.Where(t => t is not null)
|
|
.Distinct()
|
|
.ToHashSet();
|
|
|
|
// Pro Typ einmalig eine leichte {Id,Name}-Projektion laden (DB-agnostisch:
|
|
// reine Spaltenauswahl, kein ::text-Cast — läuft auf SQLite wie auf Postgres).
|
|
var byType = new Dictionary<string, List<NamedRow>>();
|
|
if (wantedTypes.Contains("tier"))
|
|
byType["tier"] = await db.Gerbils.AsNoTracking()
|
|
.Select(x => new NamedRow(x.Id, x.Name)).ToListAsync();
|
|
if (wantedTypes.Contains("kontakt"))
|
|
byType["kontakt"] = await db.Contacts.AsNoTracking()
|
|
.Select(x => new NamedRow(x.Id, x.Name)).ToListAsync();
|
|
if (wantedTypes.Contains("wurf"))
|
|
byType["wurf"] = await db.Litters.AsNoTracking()
|
|
.Select(x => new NamedRow(x.Id, x.Name)).ToListAsync();
|
|
if (wantedTypes.Contains("gehege"))
|
|
byType["gehege"] = await db.Enclosures.AsNoTracking()
|
|
.Select(x => new NamedRow(x.Id, x.Name)).ToListAsync();
|
|
|
|
var results = new List<RefResolution>(refs.Count);
|
|
foreach (var r in refs)
|
|
{
|
|
var type = NormalizeType(r.Type);
|
|
var prefix = NormalizeCode(r.Code);
|
|
|
|
if (type is null || prefix.Length < 6 || !byType.TryGetValue(type, out var rows))
|
|
{
|
|
results.Add(new RefResolution(r.Type, r.Code, null, null));
|
|
continue;
|
|
}
|
|
|
|
// Präfix-Match auf der bindestrich-losen, klein geschriebenen GUID ("N"-Format).
|
|
var matches = rows
|
|
.Where(x => x.Id.ToString("N").StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
|
.Take(2)
|
|
.ToList();
|
|
|
|
// Genau ein Treffer => auflösen; 0 (unbekannt) oder >1 (mehrdeutig) => null.
|
|
if (matches.Count == 1)
|
|
results.Add(new RefResolution(r.Type, r.Code, matches[0].Id, matches[0].Name));
|
|
else
|
|
results.Add(new RefResolution(r.Type, r.Code, null, null));
|
|
}
|
|
|
|
return TypedResults.Ok(results);
|
|
});
|
|
|
|
return app;
|
|
}
|
|
|
|
/// <summary>Akzeptiert die vier Typ-Schlüssel (case-insensitive); sonst null.</summary>
|
|
private static string? NormalizeType(string? type) => type?.Trim().ToLowerInvariant() switch
|
|
{
|
|
"tier" => "tier",
|
|
"kontakt" => "kontakt",
|
|
"wurf" => "wurf",
|
|
"gehege" => "gehege",
|
|
_ => null,
|
|
};
|
|
|
|
/// <summary>GUID-Code bindestrich-tolerant + Hex-only normalisieren (Kleinschreibung).</summary>
|
|
private static string NormalizeCode(string? code)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code)) return string.Empty;
|
|
var chars = code.Trim().ToLowerInvariant()
|
|
.Where(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'));
|
|
return new string(chars.ToArray());
|
|
}
|
|
}
|
|
}
|