Add include handling for one to one relations for library items

This commit is contained in:
Zoe Roux 2023-11-20 02:21:20 +01:00
parent 177391a74c
commit 48f82a6f13
5 changed files with 92 additions and 30 deletions

View File

@ -14,6 +14,7 @@ csharp_prefer_braces = false
dotnet_diagnostic.IDE0130.severity = none dotnet_diagnostic.IDE0130.severity = none
dotnet_diagnostic.IDE0058.severity = none dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0046.severity = none dotnet_diagnostic.IDE0046.severity = none
dotnet_diagnostic.CA1305.severity = none
dotnet_diagnostic.CA1848.severity = none dotnet_diagnostic.CA1848.severity = none
dotnet_diagnostic.CA2007.severity = none dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA1716.severity = none dotnet_diagnostic.CA1716.severity = none

View File

@ -27,5 +27,5 @@ namespace Kyoo.Abstractions.Models;
[OneOf(Types = new[] { typeof(Show), typeof(Movie), typeof(Collection) })] [OneOf(Types = new[] { typeof(Show), typeof(Movie), typeof(Collection) })]
public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate, IQuery public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate, IQuery
{ {
static Sort IQuery.DefaultSort => new Sort<ILibraryItem>.By(nameof(Movie.AirDate)); static Sort IQuery.DefaultSort => new Sort<ILibraryItem>.By(nameof(Movie.Name));
} }

View File

@ -18,6 +18,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Utils; using Kyoo.Utils;
@ -28,6 +29,7 @@ namespace Kyoo.Abstractions.Models
/// <summary> /// <summary>
/// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>. /// An actor, voice actor, writer, animator, somebody who worked on a <see cref="Show"/>.
/// </summary> /// </summary>
[Table("people")]
public class People : IQuery, IResource, IMetadata, IThumbnails public class People : IQuery, IResource, IMetadata, IThumbnails
{ {
public static Sort DefaultSort => new Sort<People>.By(x => x.Name); public static Sort DefaultSort => new Sort<People>.By(x => x.Name);

View File

@ -34,7 +34,12 @@ public class Include<T>
/// <summary> /// <summary>
/// The aditional fields to include in the result. /// The aditional fields to include in the result.
/// </summary> /// </summary>
public ICollection<string> Fields { get; private init; } = ArraySegment<string>.Empty; public ICollection<Metadata> Metadatas { get; private init; } = ArraySegment<Metadata>.Empty;
/// <summary>
/// The aditional fields names to include in the result.
/// </summary>
public ICollection<string> Fields => Metadatas.Select(x => x.Name).ToList();
public static Include<T> From(string? fields) public static Include<T> From(string? fields)
{ {
@ -43,16 +48,25 @@ public class Include<T>
return new Include<T> return new Include<T>
{ {
Fields = fields.Split(',').Select(key => Metadatas = fields.Split(',').Select<string, Metadata>(key =>
{ {
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) }; Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
PropertyInfo? prop = 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))
.FirstOrDefault(); .FirstOrDefault();
if (prop?.GetCustomAttribute<LoadableRelationAttribute>() == null) LoadableRelationAttribute? attr = prop?.GetCustomAttribute<LoadableRelationAttribute>();
if (prop == null || attr == null)
throw new ValidationException($"No loadable relation with the name {key}."); throw new ValidationException($"No loadable relation with the name {key}.");
return prop.Name; if (attr.RelationID != null)
return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID);
return new MultipleRelation(prop.Name);
}).ToArray() }).ToArray()
}; };
} }
public abstract record Metadata(string Name);
public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
public record MultipleRelation(string Name) : Metadata(Name);
} }

View File

@ -18,14 +18,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common; using System.Data.Common;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using InterpolatedSql.Dapper; using InterpolatedSql.Dapper;
using InterpolatedSql.SqlBuilders;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
@ -92,30 +96,68 @@ namespace Kyoo.Core.Controllers
throw new NotImplementedException(); throw new NotImplementedException();
} }
public string ProcessSort<T>(Sort<T> sort, Dictionary<string, Type> config) private static string _Property(string key, Dictionary<string, Type> config)
where T : IQuery
{
string Property(string key)
{ {
if (config.Count == 1) if (config.Count == 1)
return $"{config.First()}.{key.ToSnakeCase()}"; return $"{config.First()}.{key.ToSnakeCase()}";
IEnumerable<string> keys = config IEnumerable<string> keys = config
.Where(x => x.Value.GetProperty(key) != null) .Where(x => key == "id" || x.Value.GetProperty(key) != null)
.Select(x => $"{x.Key}.{key.ToSnakeCase()}"); .Select(x => $"{x.Key}.{key.ToSnakeCase()}");
return $"coalesce({string.Join(", ", keys)})"; return $"coalesce({string.Join(", ", keys)})";
} }
public static string ProcessSort<T>(Sort<T> sort, Dictionary<string, Type> config, bool recurse = false)
where T : IQuery
{
string ret = sort switch string ret = sort switch
{ {
Sort<T>.Default(var value) => ProcessSort(value, config), Sort<T>.Default(var value) => ProcessSort(value, config, true),
Sort<T>.By(string key, bool desc) => $"{Property(key)} {(desc ? "desc nulls last" : "asc")}", Sort<T>.By(string key, bool desc) => $"{_Property(key, config)} {(desc ? "desc nulls last" : "asc")}",
Sort<T>.Random(var seed) => $"md5('{seed}' || {Property("id")})", Sort<T>.Random(var seed) => $"md5('{seed}' || {_Property("id", config)})",
Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, config))), Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, config, true))),
_ => throw new SwitchExpressionException(), _ => throw new SwitchExpressionException(),
}; };
if (recurse)
return ret;
// always end query by an id sort. // always end query by an id sort.
return $"{ret}, {Property("id")} asc"; return $"{ret}, {_Property("id", config)} asc";
}
public static (
Dictionary<string, Type> config,
string join,
Func<T, IEnumerable<object>, T> map
) ProcessInclude<T>(Include<T> include, Dictionary<string, Type> config)
where T : class
{
Dictionary<string, Type> retConfig = new();
StringBuilder join = new();
foreach (Include<T>.Metadata metadata in include.Metadatas)
{
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)}");
break;
}
}
T Map(T item, IEnumerable<object> relations)
{
foreach ((string name, object value) in include.Fields.Zip(relations))
{
PropertyInfo? prop = item.GetType().GetProperty(name);
if (prop != null)
prop.SetValue(item, value);
}
return item;
}
return (retConfig, join.ToString(), Map);
} }
public async Task<ICollection<ILibraryItem>> GetAll( public async Task<ICollection<ILibraryItem>> GetAll(
@ -130,13 +172,15 @@ namespace Kyoo.Core.Controllers
{ "m", typeof(Movie) }, { "m", typeof(Movie) },
{ "c", typeof(Collection) } { "c", typeof(Collection) }
}; };
var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config);
// language=PostgreSQL // language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($""" IDapperSqlCommand query = _database.SqlBuilder($"""
select select
s.*, s.*,
m.*, m.*,
c.*, c.*
st.* {string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw}
from from
shows as s shows as s
full outer join ( full outer join (
@ -149,21 +193,22 @@ namespace Kyoo.Core.Controllers
* *
from from
collections) as c on false collections) as c on false
left join studios as st on st.id = coalesce(s.studio_id, m.studio_id) {includeJoin:raw}
order by {ProcessSort(sort, config):raw} order by {ProcessSort(sort, config):raw}
limit {limit.Limit} limit {limit.Limit}
""").Build(); """).Build();
Type[] types = config.Select(x => x.Value).Concat(new[] { typeof(Studio) }).ToArray(); Type[] types = config.Select(x => x.Value)
.Concat(includeConfig.Select(x => x.Value))
.ToArray();
IEnumerable<ILibraryItem> data = await query.QueryAsync<ILibraryItem>(types, items => IEnumerable<ILibraryItem> data = await query.QueryAsync<ILibraryItem>(types, items =>
{ {
var studio = items[3] as Studio;
if (items[0] is Show show && show.Id != 0) if (items[0] is Show show && show.Id != 0)
return show; return mapIncludes(show, items.Skip(3));
if (items[1] is Movie movie && movie.Id != 0) if (items[1] is Movie movie && movie.Id != 0)
return movie; return mapIncludes(movie, items.Skip(3));
if (items[2] is Collection collection && collection.Id != 0) if (items[2] is Collection collection && collection.Id != 0)
return collection; return mapIncludes(collection, items.Skip(3));
throw new InvalidDataException(); throw new InvalidDataException();
}); });
return data.ToList(); return data.ToList();