diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs index def9a466..bb7c2b05 100644 --- a/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs +++ b/back/src/Kyoo.Abstractions/Models/Attributes/LoadableRelationAttribute.cs @@ -35,6 +35,8 @@ namespace Kyoo.Abstractions.Models.Attributes public string? On { get; set; } + public string? Projected { get; set; } + /// /// Create a new . /// diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs index dd280c87..c8cc1e65 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Season.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Season.cs @@ -126,8 +126,20 @@ namespace Kyoo.Abstractions.Models /// /// The number of episodes in this season. /// - [Projectable(UseMemberBody = nameof(_EpisodesCount))] + [Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)] [NotMapped] + [LoadableRelation( + // language=PostgreSQL + Projected = """ + ( + select + count(*)::int + from + episodes as e + where + e.season_id = id) as episode_count + """ + )] public int EpisodesCount { get; set; } private int _EpisodesCount => Episodes!.Count; diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index 2fdcfaa2..d6dd84e2 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using EntityFrameworkCore.Projectables; using Kyoo.Abstractions.Controllers; @@ -111,6 +112,7 @@ namespace Kyoo.Abstractions.Models public string? Trailer { get; set; } [SerializeIgnore] + [Column("start_air")] public DateTime? AirDate => StartAir; /// diff --git a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs index 8b7c49e9..87218ae2 100644 --- a/back/src/Kyoo.Abstractions/Models/Utils/Include.cs +++ b/back/src/Kyoo.Abstractions/Models/Utils/Include.cs @@ -25,17 +25,28 @@ using Kyoo.Abstractions.Models.Attributes; namespace Kyoo.Abstractions.Models.Utils; -/// -/// The aditional fields to include in the result. -/// -/// The type related to the new fields -public class Include +public class Include { /// /// The aditional fields to include in the result. /// - public ICollection Metadatas { get; private init; } = ArraySegment.Empty; + public ICollection Metadatas { get; init; } = ArraySegment.Empty; + 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); + + public record ProjectedRelation(string Name, string Sql) : Metadata(Name); +} + +/// +/// The aditional fields to include in the result. +/// +/// The type related to the new fields +public class Include : Include +{ /// /// The aditional fields names to include in the result. /// @@ -79,16 +90,12 @@ public class Include // } if (attr.Sql != null) return new CustomRelation(prop.Name, prop.PropertyType, attr.Sql, attr.On, prop.DeclaringType!); + if (attr.Projected != null) + return new ProjectedRelation(prop.Name, attr.Projected); throw new NotImplementedException(); }) .Distinct(); }).ToArray() }; } - - 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 4fa4c90b..e7d07ce2 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -32,6 +32,7 @@ using InterpolatedSql.Dapper; using InterpolatedSql.SqlBuilders; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Utils; @@ -103,7 +104,7 @@ namespace Kyoo.Core.Controllers IEnumerable keys = config .Where(x => key == "id" || x.Value.GetProperty(key) != null) - .Select(x => $"{x.Key}.{key.ToSnakeCase()}"); + .Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute()?.Name ?? key.ToSnakeCase()}"); return $"coalesce({string.Join(", ", keys)})"; } @@ -140,12 +141,12 @@ namespace Kyoo.Core.Controllers relation++; switch (metadata) { - case Include.SingleRelation(var name, var type, var rid): + case Include.SingleRelation(var name, var type, var rid): string tableName = type.GetCustomAttribute()?.Name ?? $"{type.Name.ToSnakeCase()}s"; 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): + case Include.CustomRelation(var name, var type, var sql, var on, var declaring): string owner = config.First(x => x.Value == declaring).Key; string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty; sql = sql.Replace("\"this\"", owner); @@ -153,6 +154,8 @@ namespace Kyoo.Core.Controllers retConfig.Add($"r{relation}", type); join.AppendLine($"left join{lateral} ({sql}) as r{relation} on r{relation}.{on}"); break; + case Include.ProjectedRelation: + continue; default: throw new NotImplementedException(); } @@ -174,6 +177,17 @@ namespace Kyoo.Core.Controllers return (retConfig, join.ToString(), Map); } + public static string ExpendProjections(string? prefix, Include include) + { + prefix = prefix != null ? $"{prefix}." : string.Empty; + IEnumerable projections = include.Metadatas + .Select(x => x is Include.ProjectedRelation(var name, var sql) ? sql : null!) + .Where(x => x != null) + .Select(x => x.Replace("\"this\".", prefix)); + string projStr = string.Join(string.Empty, projections.Select(x => $", {x}")); + return $"{prefix}*" + projStr; + } + public async Task> GetAll( Expression>? where = null, Sort? sort = null, @@ -191,7 +205,7 @@ namespace Kyoo.Core.Controllers // language=PostgreSQL IDapperSqlCommand query = _database.SqlBuilder($""" select - s.*, + {ExpendProjections("s", include):raw}, m.*, c.* {string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw} @@ -199,12 +213,12 @@ namespace Kyoo.Core.Controllers shows as s full outer join ( select - * + {ExpendProjections(null, include):raw} from movies) as m on false - full outer join ( + full outer join ( select - * + {ExpendProjections(null, include):raw} from collections) as c on false {includeJoin:raw} diff --git a/back/src/Kyoo.Postgresql/DatabaseContext.cs b/back/src/Kyoo.Postgresql/DatabaseContext.cs index 52a8464d..b6345ac0 100644 --- a/back/src/Kyoo.Postgresql/DatabaseContext.cs +++ b/back/src/Kyoo.Postgresql/DatabaseContext.cs @@ -245,7 +245,8 @@ namespace Kyoo.Postgresql base.OnModelCreating(modelBuilder); modelBuilder.Entity() - .Ignore(x => x.FirstEpisode); + .Ignore(x => x.FirstEpisode) + .Ignore(x => x.AirDate); modelBuilder.Entity() .Ignore(x => x.PreviousEpisode) .Ignore(x => x.NextEpisode); diff --git a/front/packages/ui/src/details/season.tsx b/front/packages/ui/src/details/season.tsx index 5b8b76d0..8f36aae9 100644 --- a/front/packages/ui/src/details/season.tsx +++ b/front/packages/ui/src/details/season.tsx @@ -94,6 +94,7 @@ SeasonHeader.query = (slug: string): QueryIdentifier => params: { // Fetch all seasons at one, there won't be hundred of thems anyways. limit: 0, + fields: ["episodesCount"], }, infinite: { value: true,