diff --git a/back/.editorconfig b/back/.editorconfig index 9882c05d..fadb28c4 100644 --- a/back/.editorconfig +++ b/back/.editorconfig @@ -14,6 +14,7 @@ csharp_prefer_braces = false dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.IDE0058.severity = none dotnet_diagnostic.IDE0046.severity = none +dotnet_diagnostic.CA1305.severity = none dotnet_diagnostic.CA1848.severity = none dotnet_diagnostic.CA2007.severity = none dotnet_diagnostic.CA1716.severity = none diff --git a/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs b/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs index 00847eec..8b98aabd 100644 --- a/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs +++ b/back/src/Kyoo.Abstractions/Models/ILibraryItem.cs @@ -27,5 +27,5 @@ namespace Kyoo.Abstractions.Models; [OneOf(Types = new[] { typeof(Show), typeof(Movie), typeof(Collection) })] public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate, IQuery { - static Sort IQuery.DefaultSort => new Sort.By(nameof(Movie.AirDate)); + static Sort IQuery.DefaultSort => new Sort.By(nameof(Movie.Name)); } diff --git a/back/src/Kyoo.Abstractions/Models/Resources/People.cs b/back/src/Kyoo.Abstractions/Models/Resources/People.cs index f468840e..0c85c031 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/People.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/People.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models.Attributes; using Kyoo.Utils; @@ -28,6 +29,7 @@ namespace Kyoo.Abstractions.Models /// /// An actor, voice actor, writer, animator, somebody who worked on a . /// + [Table("people")] public class People : IQuery, IResource, IMetadata, IThumbnails { public static Sort DefaultSort => new Sort.By(x => x.Name); diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs index e94244e9..611922dd 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs @@ -34,7 +34,12 @@ public class Include /// /// The aditional fields to include in the result. /// - public ICollection Fields { get; private init; } = ArraySegment.Empty; + public ICollection Metadatas { get; private init; } = ArraySegment.Empty; + + /// + /// The aditional fields names to include in the result. + /// + public ICollection Fields => Metadatas.Select(x => x.Name).ToList(); public static Include From(string? fields) { @@ -43,16 +48,25 @@ public class Include return new Include { - Fields = fields.Split(',').Select(key => + Metadatas = fields.Split(',').Select(key => { Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; PropertyInfo? prop = types .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)) .FirstOrDefault(); - if (prop?.GetCustomAttribute() == null) + LoadableRelationAttribute? attr = prop?.GetCustomAttribute(); + if (prop == null || attr == null) throw new ValidationException($"No loadable relation with the name {key}."); - return prop.Name; + if (attr.RelationID != null) + return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID); + return new MultipleRelation(prop.Name); }).ToArray() }; } + + public abstract record Metadata(string Name); + + public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name); + + public record MultipleRelation(string Name) : Metadata(Name); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 13ef6d90..92e98b2c 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -18,14 +18,18 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; using System.Data.Common; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using System.Threading.Tasks; using Dapper; using InterpolatedSql.Dapper; +using InterpolatedSql.SqlBuilders; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Exceptions; @@ -92,30 +96,68 @@ namespace Kyoo.Core.Controllers throw new NotImplementedException(); } - public string ProcessSort(Sort sort, Dictionary config) + private static string _Property(string key, Dictionary config) + { + if (config.Count == 1) + return $"{config.First()}.{key.ToSnakeCase()}"; + + IEnumerable keys = config + .Where(x => key == "id" || x.Value.GetProperty(key) != null) + .Select(x => $"{x.Key}.{key.ToSnakeCase()}"); + return $"coalesce({string.Join(", ", keys)})"; + } + + public static string ProcessSort(Sort sort, Dictionary config, bool recurse = false) where T : IQuery { - string Property(string key) - { - if (config.Count == 1) - return $"{config.First()}.{key.ToSnakeCase()}"; - - IEnumerable keys = config - .Where(x => x.Value.GetProperty(key) != null) - .Select(x => $"{x.Key}.{key.ToSnakeCase()}"); - return $"coalesce({string.Join(", ", keys)})"; - } - string ret = sort switch { - Sort.Default(var value) => ProcessSort(value, config), - Sort.By(string key, bool desc) => $"{Property(key)} {(desc ? "desc nulls last" : "asc")}", - Sort.Random(var seed) => $"md5('{seed}' || {Property("id")})", - Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, config))), + Sort.Default(var value) => ProcessSort(value, config, true), + Sort.By(string key, bool desc) => $"{_Property(key, config)} {(desc ? "desc nulls last" : "asc")}", + Sort.Random(var seed) => $"md5('{seed}' || {_Property("id", config)})", + Sort.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, config, true))), _ => throw new SwitchExpressionException(), }; + if (recurse) + return ret; // always end query by an id sort. - return $"{ret}, {Property("id")} asc"; + return $"{ret}, {_Property("id", config)} asc"; + } + + public static ( + Dictionary config, + string join, + Func, T> map + ) ProcessInclude(Include include, Dictionary config) + where T : class + { + Dictionary retConfig = new(); + StringBuilder join = new(); + + foreach (Include.Metadata metadata in include.Metadatas) + { + switch (metadata) + { + case Include.SingleRelation(var name, var type, var rid): + string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s"; + retConfig.Add(tableName, type); + join.AppendLine($"left join {tableName} on {tableName}.id = {_Property(rid, config)}"); + break; + } + } + + T Map(T item, IEnumerable relations) + { + foreach ((string name, object value) in include.Fields.Zip(relations)) + { + PropertyInfo? prop = item.GetType().GetProperty(name); + if (prop != null) + prop.SetValue(item, value); + } + return item; + } + + return (retConfig, join.ToString(), Map); } public async Task> GetAll( @@ -130,13 +172,15 @@ namespace Kyoo.Core.Controllers { "m", typeof(Movie) }, { "c", typeof(Collection) } }; + var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config); + // language=PostgreSQL IDapperSqlCommand query = _database.SqlBuilder($""" select s.*, m.*, - c.*, - st.* + c.* + {string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw} from shows as s full outer join ( @@ -149,21 +193,22 @@ namespace Kyoo.Core.Controllers * from collections) as c on false - left join studios as st on st.id = coalesce(s.studio_id, m.studio_id) + {includeJoin:raw} order by {ProcessSort(sort, config):raw} limit {limit.Limit} """).Build(); - Type[] types = config.Select(x => x.Value).Concat(new[] { typeof(Studio) }).ToArray(); + Type[] types = config.Select(x => x.Value) + .Concat(includeConfig.Select(x => x.Value)) + .ToArray(); IEnumerable data = await query.QueryAsync(types, items => { - var studio = items[3] as Studio; if (items[0] is Show show && show.Id != 0) - return show; + return mapIncludes(show, items.Skip(3)); if (items[1] is Movie movie && movie.Id != 0) - return movie; + return mapIncludes(movie, items.Skip(3)); if (items[2] is Collection collection && collection.Id != 0) - return collection; + return mapIncludes(collection, items.Skip(3)); throw new InvalidDataException(); }); return data.ToList();