From ba83edd26c1bf36beb7a809d043343bd2baefafc Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 20 Nov 2023 23:28:50 +0100 Subject: [PATCH] Add custom relations on library items (first pass) --- back/Kyoo.ruleset | 1 + .../Attributes/LoadableRelationAttribute.cs | 4 ++ .../Models/Resources/Show.cs | 18 +++++- .../Kyoo.Abstractions/Models/Utils/Include.cs | 55 +++++++++++-------- .../Repositories/LibraryItemRepository.cs | 17 +++++- 5 files changed, 69 insertions(+), 26 deletions(-) diff --git a/back/Kyoo.ruleset b/back/Kyoo.ruleset index 9a9c6d14..60883533 100644 --- a/back/Kyoo.ruleset +++ b/back/Kyoo.ruleset @@ -2,6 +2,7 @@ + diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs index c530b0fb..def9a466 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs @@ -31,6 +31,10 @@ namespace Kyoo.Abstractions.Models.Attributes /// public string? RelationID { get; } + public string? Sql { get; set; } + + public string? On { get; set; } + /// /// Create a new . /// diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index 8d8c888c..9ad9cf00 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -152,7 +152,23 @@ namespace Kyoo.Abstractions.Models /// The first episode of this show. /// [Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)] - [LoadableRelation] public Episode? FirstEpisode { get; set; } + [LoadableRelation( + // language=PostgreSQL + Sql = """ + select + fe.* + from ( + select + e.*, + row_number() over (partition by e.show_id order by e.absolute_number, e.season_number, e.episode_number) as number + from + episodes as e) as fe + where + fe.number <= 1 + """, + On = "show_id" + )] + public Episode? FirstEpisode { get; set; } private Episode? _FirstEpisode => Episodes! .OrderBy(x => x.AbsoluteNumber) diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs index 51549c01..e434e64c 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs @@ -17,7 +17,6 @@ // along with Kyoo. If not, see . using System; -using System.Collections; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -47,32 +46,42 @@ public class Include if (string.IsNullOrEmpty(fields)) return new Include(); + Type[] types = typeof(T).GetCustomAttribute()?.Types ?? new[] { typeof(T) }; return new Include { - Metadatas = fields.Split(',').Select(key => + Metadatas = fields.Split(',').SelectMany(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(); - LoadableRelationAttribute? attr = prop?.GetCustomAttribute(); - if (prop == null || attr == null) + var relations = types + .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!) + .Select(prop => (prop, attr: prop?.GetCustomAttribute()!)) + .Where(x => x.prop != null && x.attr != null) + .ToList(); + if (!relations.Any()) throw new ValidationException($"No loadable relation with the name {key}."); - if (attr.RelationID != null) - return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID); + return relations + .Select(x => + { + (PropertyInfo prop, LoadableRelationAttribute attr) = x; - // Multiples relations are disabled due to: - // - Cartesian Explosions perfs - // - Code complexity added. - // if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string)) - // { - // // The property is either a list or a an array. - // return new MultipleRelation( - // prop.Name, - // prop.PropertyType.GetElementType() ?? prop.PropertyType.GenericTypeArguments.First() - // ); - // } - throw new NotImplementedException(); + if (attr.RelationID != null) + return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata; + + // Multiples relations are disabled due to: + // - Cartesian Explosions perfs + // - Code complexity added. + // if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string)) + // { + // // The property is either a list or a an array. + // return new MultipleRelation( + // prop.Name, + // prop.PropertyType.GetElementType() ?? prop.PropertyType.GenericTypeArguments.First() + // ); + // } + if (attr.Sql != null && attr.On != null) + return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!); + throw new NotImplementedException(); + }) + .Distinct(); }).ToArray() }; } @@ -80,4 +89,6 @@ public class Include public abstract record Metadata(string Name); public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name); + + public record CustomRelation(string Name, Type type, string Sql, string On, Type Declaring) : Metadata(Name); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 92e98b2c..f150a47d 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -131,25 +131,36 @@ namespace Kyoo.Core.Controllers ) ProcessInclude(Include include, Dictionary config) where T : class { + int relation = 0; Dictionary retConfig = new(); StringBuilder join = new(); foreach (Include.Metadata metadata in include.Metadatas) { + relation++; 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)}"); + retConfig.Add($"r{relation}", type); + join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}"); break; + case Include.CustomRelation(var name, var type, var sql, var on, var declaring): + string owner = config.First(x => x.Value == declaring).Key; + retConfig.Add($"r{relation}", type); + join.AppendLine($"left join ({sql}) as r{relation} on r{relation}.{on} = {owner}.id"); + break; + default: + throw new NotImplementedException(); } } T Map(T item, IEnumerable relations) { - foreach ((string name, object value) in include.Fields.Zip(relations)) + foreach ((string name, object? value) in include.Fields.Zip(relations)) { + if (value == null) + continue; PropertyInfo? prop = item.GetType().GetProperty(name); if (prop != null) prop.SetValue(item, value);