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">
<Rule Id="SA1413" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
<Rule Id="SA1414" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
<Rule Id="SA1114" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
</Rules>
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
<Rule Id="SA1201" Action="None" /> <!-- ElementsMustAppearInTheCorrectOrder -->

View File

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

View File

@ -152,7 +152,23 @@ namespace Kyoo.Abstractions.Models
/// The first episode of this show.
/// </summary>
[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)

View File

@ -17,7 +17,6 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -47,32 +46,42 @@ public class Include<T>
if (string.IsNullOrEmpty(fields))
return new Include<T>();
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(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) };
PropertyInfo? prop = types
.Select(x => x.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance))
.FirstOrDefault();
LoadableRelationAttribute? attr = prop?.GetCustomAttribute<LoadableRelationAttribute>();
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<LoadableRelationAttribute>()!))
.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<T>
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);
}

View File

@ -131,25 +131,36 @@ namespace Kyoo.Core.Controllers
) ProcessInclude<T>(Include<T> include, Dictionary<string, Type> config)
where T : class
{
int relation = 0;
Dictionary<string, Type> retConfig = new();
StringBuilder join = new();
foreach (Include<T>.Metadata metadata in include.Metadatas)
{
relation++;
switch (metadata)
{
case Include<T>.SingleRelation(var name, var type, var rid):
string tableName = type.GetCustomAttribute<TableAttribute>()?.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<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)
{
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);