Add custom relations on library items (first pass)

This commit is contained in:
Zoe Roux 2023-11-20 23:28:50 +01:00
parent eed058c891
commit ba83edd26c
5 changed files with 69 additions and 26 deletions

View File

@ -2,6 +2,7 @@
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.MaintainabilityRules"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.MaintainabilityRules">
<Rule Id="SA1413" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers --> <Rule Id="SA1413" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
<Rule Id="SA1414" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers --> <Rule Id="SA1414" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
<Rule Id="SA1114" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
</Rules> </Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules"> <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
<Rule Id="SA1201" Action="None" /> <!-- ElementsMustAppearInTheCorrectOrder --> <Rule Id="SA1201" Action="None" /> <!-- ElementsMustAppearInTheCorrectOrder -->

View File

@ -31,6 +31,10 @@ namespace Kyoo.Abstractions.Models.Attributes
/// </summary> /// </summary>
public string? RelationID { get; } public string? RelationID { get; }
public string? Sql { get; set; }
public string? On { get; set; }
/// <summary> /// <summary>
/// Create a new <see cref="LoadableRelationAttribute"/>. /// Create a new <see cref="LoadableRelationAttribute"/>.
/// </summary> /// </summary>

View File

@ -152,7 +152,23 @@ namespace Kyoo.Abstractions.Models
/// The first episode of this show. /// The first episode of this show.
/// </summary> /// </summary>
[Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)] [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! private Episode? _FirstEpisode => Episodes!
.OrderBy(x => x.AbsoluteNumber) .OrderBy(x => x.AbsoluteNumber)

View File

@ -17,7 +17,6 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
@ -47,32 +46,42 @@ public class Include<T>
if (string.IsNullOrEmpty(fields)) if (string.IsNullOrEmpty(fields))
return new Include<T>(); return new Include<T>();
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
return new Include<T> return new Include<T>
{ {
Metadatas = fields.Split(',').Select<string, Metadata>(key => Metadatas = fields.Split(',').SelectMany(key =>
{ {
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) }; var relations = types
PropertyInfo? prop = types .Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)!)
.Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance)) .Select(prop => (prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!))
.FirstOrDefault(); .Where(x => x.prop != null && x.attr != null)
LoadableRelationAttribute? attr = prop?.GetCustomAttribute<LoadableRelationAttribute>(); .ToList();
if (prop == null || attr == null) if (!relations.Any())
throw new ValidationException($"No loadable relation with the name {key}."); throw new ValidationException($"No loadable relation with the name {key}.");
if (attr.RelationID != null) return relations
return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID); .Select(x =>
{
(PropertyInfo prop, LoadableRelationAttribute attr) = x;
// Multiples relations are disabled due to: if (attr.RelationID != null)
// - Cartesian Explosions perfs return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID) as Metadata;
// - Code complexity added.
// if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string)) // Multiples relations are disabled due to:
// { // - Cartesian Explosions perfs
// // The property is either a list or a an array. // - Code complexity added.
// return new MultipleRelation( // if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) && prop.PropertyType != typeof(string))
// prop.Name, // {
// prop.PropertyType.GetElementType() ?? prop.PropertyType.GenericTypeArguments.First() // // The property is either a list or a an array.
// ); // return new MultipleRelation(
// } // prop.Name,
throw new NotImplementedException(); // 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() }).ToArray()
}; };
} }
@ -80,4 +89,6 @@ public class Include<T>
public abstract record Metadata(string Name); public abstract record Metadata(string Name);
public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(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);
} }

View File

@ -131,25 +131,36 @@ namespace Kyoo.Core.Controllers
) ProcessInclude<T>(Include<T> include, Dictionary<string, Type> config) ) ProcessInclude<T>(Include<T> include, Dictionary<string, Type> config)
where T : class where T : class
{ {
int relation = 0;
Dictionary<string, Type> retConfig = new(); Dictionary<string, Type> retConfig = new();
StringBuilder join = new(); StringBuilder join = new();
foreach (Include<T>.Metadata metadata in include.Metadatas) foreach (Include<T>.Metadata metadata in include.Metadatas)
{ {
relation++;
switch (metadata) switch (metadata)
{ {
case Include<T>.SingleRelation(var name, var type, var rid): case Include<T>.SingleRelation(var name, var type, var rid):
string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s"; string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
retConfig.Add(tableName, type); retConfig.Add($"r{relation}", type);
join.AppendLine($"left join {tableName} on {tableName}.id = {_Property(rid, config)}"); join.AppendLine($"left join {tableName} as r{relation} on r{relation}.id = {_Property(rid, config)}");
break; break;
case Include<T>.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<object> relations) T Map(T item, IEnumerable<object> 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); PropertyInfo? prop = item.GetType().GetProperty(name);
if (prop != null) if (prop != null)
prop.SetValue(item, value); prop.SetValue(item, value);