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