using HyperBooru.ApiModels; using Microsoft.EntityFrameworkCore; namespace HyperBooru.Services; public interface ITagService { public void AddTag(Guid obj, Guid tagDef); public void AddTag(HBObject obj, TagDefinition tagDef); public void RemoveTag(Guid obj, Guid tagDef); public void RemoveTag(HBObject obj, TagDefinition tagDef); public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs); public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs); public void AddImplicitTag(Guid tagDef, Guid implicitTagDef); public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef); public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef); public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null); public void DeleteTagDefinition(Guid tagDef); public void DeleteTagDefinition(TagDefinition tagDef); public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null); public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef); public TagDefinition[] TagsThatImply(Guid tagDef); public TagDefinition[] TagsThatImply(TagDefinition tagDef); } public class TagService : ITagService { private IDbContextFactory dbFactory; public TagService(IDbContextFactory dbFactory) => this.dbFactory = dbFactory; public void AddTag(Guid obj, Guid tagDef) { using var db = dbFactory.CreateDbContext(); var tag = db.TagDefinitions.First(td => td.Guid == tagDef); db.Objects .Include(o => o.Tags) .ThenInclude(t => t.TagDefinition) .Where(o => !o.Tags.Select(t => t.TagDefinition.Guid).Contains(tagDef)) .FirstOrDefault(o => o.Guid == obj)? .Tags .Add(new(tag)); db.SaveChanges(); } public void AddTag(HBObject obj, TagDefinition tagDef) => AddTag(obj.Guid, tagDef.Guid); public void RemoveTag(Guid obj, Guid tagDef) { using var db = dbFactory.CreateDbContext(); db.Objects .Include(o => o.Tags) .ThenInclude(t => t.TagDefinition) .First(o => o.Guid == obj) .Tags .RemoveAll(t => t.TagDefinition.Guid == tagDef); db.SaveChanges(); } public void RemoveTag(HBObject obj, TagDefinition tagDef) => RemoveTag(obj.Guid, tagDef.Guid); public void SetImplicitTags(Guid tagDef, Guid[] implicitTagDefs) { using var db = dbFactory.CreateDbContext(); using var transaction = db.Database.BeginTransaction(); var tag = db.TagDefinitions .Include(td => td.ImplicitTags) .First(td => td.Guid == tagDef); tag.ImplicitTags.RemoveAll(td => !implicitTagDefs.Contains(td.Guid)); tag.ImplicitTags.AddRange( db.TagDefinitions .Where(td => implicitTagDefs.Contains(td.Guid)) .Where(td => !tag.ImplicitTags .Select(td => td.Guid) .Contains(td.Guid))); db.SaveChanges(); transaction.Commit(); } public void SetImplicitTags(TagDefinition tagDef, TagDefinition[] implicitTagDefs) => SetImplicitTags(tagDef.Guid, implicitTagDefs.Select(td => td.Guid).ToArray()); public void AddImplicitTag(Guid tagDef, Guid implicitTagDef) { using var db = dbFactory.CreateDbContext(); var tag = db.TagDefinitions .Include(td => td.ImplicitTags) .First(td => td.Guid == tagDef); var implicitTag = db.TagDefinitions.First(td => td.Guid == implicitTagDef); tag.ImplicitTags.Add(implicitTag); db.SaveChanges(); } public void AddImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => AddImplicitTag(tagDef, implicitTagDef); public void RemoveImplicitTag(Guid tagDef, Guid implicitTagDef) { using var db = dbFactory.CreateDbContext(); var tag = db.TagDefinitions .Include(td => td.ImplicitTags) .First(td => td.Guid == tagDef); tag.ImplicitTags.RemoveAll(td => td.Guid == implicitTagDef); db.SaveChanges(); } public void RemoveImplicitTag(TagDefinition tagDef, TagDefinition implicitTagDef) => RemoveImplicitTag(tagDef, implicitTagDef); public void CreateTagDefinition(string name, string? @namespace = null, string? alias = null) { using var db = dbFactory.CreateDbContext(); if(string.IsNullOrEmpty(@namespace)) @namespace = null; if(string.IsNullOrEmpty(alias)) alias = null; // Remove leading and trailing whitespace name = name.Trim(); @namespace = @namespace?.Trim(); alias = alias?.Trim(); TagDefinition tagDef = new() { Source = TagSource.UserTag, Namespace = @namespace, Name = name, Alias = alias }; bool nameExists = db.TagDefinitions.Any(td => td.Name.ToLower() == name.ToLower()); bool aliasExists = false; if(alias is not null) aliasExists = db.TagDefinitions .Where(td => td.Alias != null) .Any(td => td.Alias!.ToLower() == alias.ToLower()); if(nameExists || aliasExists) throw new TagDuplicateException(nameExists, aliasExists); if(!db.TagDefinitions.Contains(tagDef)) db.TagDefinitions.Add(tagDef); db.SaveChanges(); } public void DeleteTagDefinition(Guid tagDef) { using var db = dbFactory.CreateDbContext(); var tag = db.TagDefinitions.First(td => td.Guid == tagDef); using var transaction = db.Database.BeginTransaction(); db.Tags.RemoveRange( db.Tags .Include(t => t.TagDefinition) .Where(t => t.TagDefinition.Guid == tagDef)); db.TagDefinitions.Remove(tag); db.SaveChanges(); transaction.Commit(); } public void DeleteTagDefinition(TagDefinition tagDef) => DeleteTagDefinition(tagDef.Guid); public void UpdateTagDefinition(Guid tagDef, string name, string? @namespace = null, string? alias = null) { using var db = dbFactory.CreateDbContext(); if(string.IsNullOrEmpty(@namespace)) @namespace = null; if(string.IsNullOrEmpty(alias)) alias = null; // Remove leading and trailing whitespace name = name.Trim(); @namespace = @namespace?.Trim(); alias = alias?.Trim(); var tag = db.TagDefinitions.First(td => td.Guid == tagDef); TagDefinition? nameExisting = db.TagDefinitions.FirstOrDefault(td => td.Name.ToLower() == name.ToLower()); TagDefinition? aliasExisting = null; if(alias is not null) aliasExisting = db.TagDefinitions .Where(td => td.Alias != null) .FirstOrDefault(td => td.Alias!.ToLower() == alias.ToLower()); bool nameExists = nameExisting is not null && nameExisting != tag; bool aliasExists = aliasExisting is not null && aliasExisting != tag; if(nameExists || aliasExists) throw new TagDuplicateException(nameExists, aliasExists); tag.Name = name; tag.Namespace = @namespace; tag.Alias = alias; db.SaveChanges(); } public void UpdateTagDefinition(TagDefinition tagDef, string name, string? @namespace = null, string? alias = null) => UpdateTagDefinition(tagDef.Guid, name, @namespace, alias); private (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(IEnumerable tagDefs) { using var db = dbFactory.CreateDbContext(); var tagGuids = tagDefs .Select(td => td.Guid) .ToArray(); // Query all tag definitions var allTags = db.TagDefinitions .Include(td => td.ImplicitTags) .ToArray(); var tags = new List( allTags.IntersectBy( tagGuids, td => td.Guid)); while(true) { var toAdd = tags .SelectMany(td => td.ImplicitTags) .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) .ToArray(); if(toAdd.Count() == 0) break; tags.AddRange(toAdd); } return tags .Select(td => new ValueTuple(td, !tagGuids.Contains(td.Guid))) .ToArray(); } public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(Guid obj) { using var db = dbFactory.CreateDbContext(); // Query a list of tag GUIDs for this object var tags = db.Objects .Include(o => o.Tags) .ThenInclude(t => t.TagDefinition) .First(o => o.Guid == obj) .Tags .Select(t => t.TagDefinition) .ToArray(); return GetAllTags(tags); } public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(HBObject obj) => GetAllTags(obj.Guid); public (TagDefinition tagDefinition, bool isImplicit)[] GetAllTags(TagDefinition tagDef) { using var db = dbFactory.CreateDbContext(); var tags = db.TagDefinitions .Include(td => td.ImplicitTags) .First(td => td.Guid == tagDef.Guid) .ImplicitTags .ToArray(); return GetAllTags(tags); } public TagDefinition[] TagsThatImply(Guid tagDef) { using var db = dbFactory.CreateDbContext(); var tagDefs = db.TagDefinitions .Include(td => td.ImplicitTags) .ToArray(); var tags = new List() { db.TagDefinitions.First(td => td.Guid == tagDef) }; while(true) { var toAdd = tagDefs .Where(td => td.ImplicitTags.Select(it => it.Guid).Intersect(tags.Select(td => td.Guid)).Any()) .ExceptBy(tags.Select(td => td.Guid), td => td.Guid) .ToArray(); if(toAdd.Count() == 0) break; tags.AddRange(toAdd); } return tags.ToArray(); } public TagDefinition[] TagsThatImply(TagDefinition tagDef) => TagsThatImply(tagDef.Guid); }