mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-23 17:52:36 -04:00
Create a helper class to make queries eaiser
This commit is contained in:
parent
067eafbbe4
commit
411054afe9
@ -2,3 +2,4 @@ tabs=1
|
|||||||
function-case=1 #lowercase
|
function-case=1 #lowercase
|
||||||
keyword-case=1
|
keyword-case=1
|
||||||
type-case=1
|
type-case=1
|
||||||
|
no-space-function=1
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
<PropertyGroup Condition="$(CheckCodingStyle) == true">
|
<PropertyGroup Condition="$(CheckCodingStyle) == true">
|
||||||
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
|
||||||
<NoWarn>1591;1305;8618</NoWarn>
|
<NoWarn>1591;1305;8618;SYSLIB1045</NoWarn>
|
||||||
<!-- <AnalysisMode>All</AnalysisMode> -->
|
<!-- <AnalysisMode>All</AnalysisMode> -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ namespace Kyoo.Abstractions.Controllers
|
|||||||
Task<ICollection<T>> GetAll(Filter<T>? filter = null,
|
Task<ICollection<T>> GetAll(Filter<T>? filter = null,
|
||||||
Sort<T>? sort = default,
|
Sort<T>? sort = default,
|
||||||
Include<T>? include = default,
|
Include<T>? include = default,
|
||||||
Pagination limit = default);
|
Pagination? limit = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the number of resources that match the filter's predicate.
|
/// Get the number of resources that match the filter's predicate.
|
||||||
|
@ -174,7 +174,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
Sql = """
|
Sql = """
|
||||||
select
|
select
|
||||||
"pe".*,
|
"pe".* -- Episode as pe
|
||||||
from
|
from
|
||||||
episodes as "pe"
|
episodes as "pe"
|
||||||
where
|
where
|
||||||
@ -210,7 +210,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
Sql = """
|
Sql = """
|
||||||
select
|
select
|
||||||
"ne".*,
|
"ne".* -- Episode as ne
|
||||||
from
|
from
|
||||||
episodes as "ne"
|
episodes as "ne"
|
||||||
where
|
where
|
||||||
|
@ -158,7 +158,7 @@ namespace Kyoo.Abstractions.Models
|
|||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
Sql = """
|
Sql = """
|
||||||
select
|
select
|
||||||
"fe".*
|
"fe".* -- Episode as fe
|
||||||
from (
|
from (
|
||||||
select
|
select
|
||||||
e.*,
|
e.*,
|
||||||
|
245
back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
Normal file
245
back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using InterpolatedSql.Dapper;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public static class DapperHelper
|
||||||
|
{
|
||||||
|
private static string _Property(string key, Dictionary<string, Type> config)
|
||||||
|
{
|
||||||
|
if (config.Count == 1)
|
||||||
|
return $"{config.First()}.{key.ToSnakeCase()}";
|
||||||
|
|
||||||
|
IEnumerable<string> keys = config
|
||||||
|
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
|
||||||
|
.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
|
||||||
|
return $"coalesce({string.Join(", ", keys)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ProcessSort<T>(Sort<T>? sort, bool reverse, Dictionary<string, Type> config, bool recurse = false)
|
||||||
|
where T : IQuery
|
||||||
|
{
|
||||||
|
sort ??= new Sort<T>.Default();
|
||||||
|
|
||||||
|
string ret = sort switch
|
||||||
|
{
|
||||||
|
Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
|
||||||
|
Sort<T>.By(string key, bool desc) => $"{_Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
|
||||||
|
Sort<T>.Random(var seed) => $"md5('{seed}' || {_Property("id", config)}) {(reverse ? "desc" : "asc")}",
|
||||||
|
Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
|
||||||
|
_ => throw new SwitchExpressionException(),
|
||||||
|
};
|
||||||
|
if (recurse)
|
||||||
|
return ret;
|
||||||
|
// always end query by an id sort.
|
||||||
|
return $"{ret}, {_Property("id", config)} {(reverse ? "desc" : "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
|
||||||
|
{
|
||||||
|
int relation = 0;
|
||||||
|
Dictionary<string, Type> retConfig = new();
|
||||||
|
StringBuilder join = new();
|
||||||
|
|
||||||
|
foreach (Include<T>.Metadata metadata in include.Metadatas)
|
||||||
|
{
|
||||||
|
relation++;
|
||||||
|
switch (metadata)
|
||||||
|
{
|
||||||
|
case Include.SingleRelation(var name, var type, var rid):
|
||||||
|
string tableName = type.GetCustomAttribute<TableAttribute>()?.Name ?? $"{type.Name.ToSnakeCase()}s";
|
||||||
|
retConfig.Add($"r{relation}", type);
|
||||||
|
join.Append($"\nleft 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;
|
||||||
|
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
|
||||||
|
sql = sql.Replace("\"this\"", owner);
|
||||||
|
on = on?.Replace("\"this\"", owner);
|
||||||
|
retConfig.Add($"r{relation}", type);
|
||||||
|
join.Append($"\nleft join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
|
||||||
|
break;
|
||||||
|
case Include.ProjectedRelation:
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T Map(T item, IEnumerable<object> 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);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (retConfig, join.ToString(), Map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FormattableString ProcessFilter<T>(Filter<T> filter, Dictionary<string, Type> config)
|
||||||
|
{
|
||||||
|
FormattableString Format(string key, FormattableString op)
|
||||||
|
{
|
||||||
|
IEnumerable<string> properties = config
|
||||||
|
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
|
||||||
|
.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
|
||||||
|
|
||||||
|
FormattableString ret = $"{properties.First():raw} {op}";
|
||||||
|
foreach (string property in properties.Skip(1))
|
||||||
|
ret = $"{ret} or {property:raw} {op}";
|
||||||
|
return $"({ret})";
|
||||||
|
}
|
||||||
|
|
||||||
|
object P(object value)
|
||||||
|
{
|
||||||
|
if (value is Enum)
|
||||||
|
return new Wrapper(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormattableString Process(Filter<T> fil)
|
||||||
|
{
|
||||||
|
return fil switch
|
||||||
|
{
|
||||||
|
Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
|
||||||
|
Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
|
||||||
|
Filter<T>.Not(var inner) => $"(not {Process(inner)})",
|
||||||
|
Filter<T>.Eq(var property, var value) when value is null => Format(property, $"is null"),
|
||||||
|
Filter<T>.Ne(var property, var value) when value is null => Format(property, $"is not null"),
|
||||||
|
Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value!)}"),
|
||||||
|
Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value!)}"),
|
||||||
|
Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
|
||||||
|
Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
|
||||||
|
Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
|
||||||
|
Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
|
||||||
|
Filter<T>.Has(var property, var value) => $"{P(value)} = any({_Property(property, config):raw})",
|
||||||
|
Filter<T>.EqRandom(var seed, var id) => $"md5({seed} || {config.Select(x => $"{x.Key}.id"):raw}) = md5({seed} || {id.ToString()})",
|
||||||
|
Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return $"\nwhere {Process(filter)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExpendProjections(string type, string? prefix, Include include)
|
||||||
|
{
|
||||||
|
prefix = prefix != null ? $"{prefix}." : string.Empty;
|
||||||
|
IEnumerable<string> 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));
|
||||||
|
return string.Join(string.Empty, projections.Select(x => $", {x}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ICollection<T>> Query<T>(
|
||||||
|
this IDbConnection db,
|
||||||
|
FormattableString command,
|
||||||
|
Dictionary<string, Type> config,
|
||||||
|
Func<object?[], T> mapper,
|
||||||
|
Func<int, Task<T>> get,
|
||||||
|
Include<T>? include,
|
||||||
|
Filter<T>? filter,
|
||||||
|
Sort<T>? sort,
|
||||||
|
Pagination limit)
|
||||||
|
where T : class, IResource, IQuery
|
||||||
|
{
|
||||||
|
InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command);
|
||||||
|
|
||||||
|
// Include handling
|
||||||
|
include ??= new();
|
||||||
|
var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config);
|
||||||
|
query.AppendLiteral(includeJoin);
|
||||||
|
string includeProjection = string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*"));
|
||||||
|
query.Replace("/* includes */", $"{includeProjection:raw}", out bool replaced);
|
||||||
|
if (!replaced)
|
||||||
|
throw new ArgumentException("Missing '/* includes */' placeholder in top level sql select to support includes.");
|
||||||
|
|
||||||
|
// Handle pagination, orders and filter.
|
||||||
|
if (limit.AfterID != null)
|
||||||
|
{
|
||||||
|
T reference = await get(limit.AfterID.Value);
|
||||||
|
Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
|
||||||
|
filter = Filter.And(filter, keysetFilter);
|
||||||
|
}
|
||||||
|
if (filter != null)
|
||||||
|
query += ProcessFilter(filter, config);
|
||||||
|
query += $"\norder by {ProcessSort(sort, limit.Reverse, config):raw}";
|
||||||
|
query += $"\nlimit {limit.Limit}";
|
||||||
|
|
||||||
|
// Build query and prepare to do the query/projections
|
||||||
|
IDapperSqlCommand cmd = query.Build();
|
||||||
|
string sql = cmd.Sql;
|
||||||
|
Type[] types = config.Select(x => x.Value)
|
||||||
|
.Concat(includeConfig.Select(x => x.Value))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
// Expand projections on every types received.
|
||||||
|
sql = Regex.Replace(sql, @"(,?) -- (\w+)( as (\w+))?", (match) =>
|
||||||
|
{
|
||||||
|
string leadingComa = match.Groups[1].Value;
|
||||||
|
string type = match.Groups[2].Value;
|
||||||
|
string? prefix = match.Groups[3].Value;
|
||||||
|
|
||||||
|
// Only project top level items with explicit includes.
|
||||||
|
string? projection = config.Any(x => x.Value.Name == type)
|
||||||
|
? ExpendProjections(type, prefix, include)
|
||||||
|
: null;
|
||||||
|
if (string.IsNullOrEmpty(projection))
|
||||||
|
return leadingComa;
|
||||||
|
return $", {projection}{leadingComa}";
|
||||||
|
});
|
||||||
|
|
||||||
|
IEnumerable<T> data = await db.QueryAsync<T>(
|
||||||
|
sql,
|
||||||
|
types,
|
||||||
|
items => mapIncludes(mapper(items), items.Skip(config.Count)),
|
||||||
|
ParametersDictionary.LoadFrom(cmd)
|
||||||
|
);
|
||||||
|
if (limit.Reverse)
|
||||||
|
data = data.Reverse();
|
||||||
|
return data.ToList();
|
||||||
|
}
|
||||||
|
}
|
104
back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
Normal file
104
back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public class DapperRepository<T> : IRepository<T>
|
||||||
|
where T : class, IResource, IQuery
|
||||||
|
{
|
||||||
|
public Type RepositoryType => typeof(T);
|
||||||
|
|
||||||
|
public Task<ICollection<T>> FromIds(IList<int> ids, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Get(int id, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Get(string slug, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Get(Filter<T> filter, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ICollection<T>> GetAll(Filter<T>? filter = null, Sort<T>? sort = null, Include<T>? include = null, Pagination? limit = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<int> GetCount(Filter<T>? filter = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T?> GetOrDefault(int id, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T?> GetOrDefault(string slug, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T?> GetOrDefault(Filter<T>? filter, Include<T>? include = null, Sort<T>? sortBy = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<ICollection<T>> Search(string query, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<T> Create(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<T> CreateIfNotExists(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task Delete(int id) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task Delete(string slug) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task Delete(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task DeleteAll(Filter<T> filter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<T> Edit(T edited) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<T> Patch(int id, Func<T, Task<bool>> patch) => throw new NotImplementedException();
|
||||||
|
}
|
@ -95,208 +95,52 @@ namespace Kyoo.Core.Controllers
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string _Property(string key, Dictionary<string, Type> config)
|
public Task<ICollection<ILibraryItem>> GetAll(
|
||||||
{
|
Filter<ILibraryItem>? filter = null,
|
||||||
if (config.Count == 1)
|
|
||||||
return $"{config.First()}.{key.ToSnakeCase()}";
|
|
||||||
|
|
||||||
IEnumerable<string> keys = config
|
|
||||||
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
|
|
||||||
.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
|
|
||||||
return $"coalesce({string.Join(", ", keys)})";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ProcessSort<T>(Sort<T>? sort, bool reverse, Dictionary<string, Type> config, bool recurse = false)
|
|
||||||
where T : IQuery
|
|
||||||
{
|
|
||||||
sort ??= new Sort<T>.Default();
|
|
||||||
|
|
||||||
string ret = sort switch
|
|
||||||
{
|
|
||||||
Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
|
|
||||||
Sort<T>.By(string key, bool desc) => $"{_Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
|
|
||||||
Sort<T>.Random(var seed) => $"md5('{seed}' || {_Property("id", config)}) {(reverse ? "desc" : "asc")}",
|
|
||||||
Sort<T>.Conglomerate(var list) => string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
|
|
||||||
_ => throw new SwitchExpressionException(),
|
|
||||||
};
|
|
||||||
if (recurse)
|
|
||||||
return ret;
|
|
||||||
// always end query by an id sort.
|
|
||||||
return $"{ret}, {_Property("id", config)} {(reverse ? "desc" : "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
|
|
||||||
{
|
|
||||||
int relation = 0;
|
|
||||||
Dictionary<string, Type> retConfig = new();
|
|
||||||
StringBuilder join = new();
|
|
||||||
|
|
||||||
foreach (Include<T>.Metadata metadata in include.Metadatas)
|
|
||||||
{
|
|
||||||
relation++;
|
|
||||||
switch (metadata)
|
|
||||||
{
|
|
||||||
case Include.SingleRelation(var name, var type, var rid):
|
|
||||||
string tableName = type.GetCustomAttribute<TableAttribute>()?.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):
|
|
||||||
string owner = config.First(x => x.Value == declaring).Key;
|
|
||||||
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
|
|
||||||
sql = sql.Replace("\"this\"", owner);
|
|
||||||
on = on?.Replace("\"this\"", owner);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
T Map(T item, IEnumerable<object> 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);
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (retConfig, join.ToString(), Map);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FormattableString ExpendProjections<T>(string? prefix, Include include)
|
|
||||||
{
|
|
||||||
prefix = prefix != null ? $"{prefix}." : string.Empty;
|
|
||||||
IEnumerable<string> 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:raw}*{projStr:raw}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static FormattableString ProcessFilter<T>(Filter<T> filter, Dictionary<string, Type> config)
|
|
||||||
{
|
|
||||||
FormattableString Format(string key, FormattableString op)
|
|
||||||
{
|
|
||||||
IEnumerable<string> properties = config
|
|
||||||
.Where(x => key == "id" || x.Value.GetProperty(key) != null)
|
|
||||||
.Select(x => $"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}");
|
|
||||||
|
|
||||||
FormattableString ret = $"{properties.First():raw} {op}";
|
|
||||||
foreach (string property in properties.Skip(1))
|
|
||||||
ret = $"{ret} or {property:raw} {op}";
|
|
||||||
return $"({ret})";
|
|
||||||
}
|
|
||||||
|
|
||||||
object P(object value)
|
|
||||||
{
|
|
||||||
if (value is Enum)
|
|
||||||
return new Wrapper(value);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
FormattableString Process(Filter<T> fil)
|
|
||||||
{
|
|
||||||
return fil switch
|
|
||||||
{
|
|
||||||
Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
|
|
||||||
Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
|
|
||||||
Filter<T>.Not(var inner) => $"(not {Process(inner)})",
|
|
||||||
Filter<T>.Eq(var property, var value) when value is null => Format(property, $"is null"),
|
|
||||||
Filter<T>.Ne(var property, var value) when value is null => Format(property, $"is not null"),
|
|
||||||
Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value!)}"),
|
|
||||||
Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value!)}"),
|
|
||||||
Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
|
|
||||||
Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
|
|
||||||
Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
|
|
||||||
Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
|
|
||||||
Filter<T>.Has(var property, var value) => $"{P(value)} = any({_Property(property, config):raw})",
|
|
||||||
Filter<T>.EqRandom(var seed, var id) => $"md5({seed} || {config.Select(x => $"{x.Key}.id"):raw}) = md5({seed} || {id.ToString()})",
|
|
||||||
Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
|
|
||||||
_ => throw new NotImplementedException(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return $"where {Process(filter)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<ICollection<ILibraryItem>> GetAll(Filter<ILibraryItem>? filter = null,
|
|
||||||
Sort<ILibraryItem>? sort = default,
|
Sort<ILibraryItem>? sort = default,
|
||||||
Include<ILibraryItem>? include = default,
|
Include<ILibraryItem>? include = default,
|
||||||
Pagination limit = default)
|
Pagination? limit = default)
|
||||||
{
|
{
|
||||||
include ??= new();
|
|
||||||
|
|
||||||
Dictionary<string, Type> config = new()
|
|
||||||
{
|
|
||||||
{ "s", typeof(Show) },
|
|
||||||
{ "m", typeof(Movie) },
|
|
||||||
{ "c", typeof(Collection) }
|
|
||||||
};
|
|
||||||
var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config);
|
|
||||||
|
|
||||||
// language=PostgreSQL
|
// language=PostgreSQL
|
||||||
var query = _database.SqlBuilder($"""
|
FormattableString sql = $"""
|
||||||
select
|
select
|
||||||
{ExpendProjections<Show>("s", include)},
|
s.*, -- Show as s
|
||||||
m.*,
|
m.*,
|
||||||
c.*
|
c.*
|
||||||
{string.Join(string.Empty, includeConfig.Select(x => $", {x.Key}.*")):raw}
|
/* includes */
|
||||||
from
|
from
|
||||||
shows as s
|
shows as s
|
||||||
full outer join (
|
full outer join (
|
||||||
select
|
select
|
||||||
{ExpendProjections<Movie>(null, include)}
|
* -- Movie
|
||||||
from
|
from
|
||||||
movies) as m on false
|
movies) as m on false
|
||||||
full outer join (
|
full outer join (
|
||||||
select
|
select
|
||||||
{ExpendProjections<Collection>(null, include)}
|
* -- Collection
|
||||||
from
|
from
|
||||||
collections) as c on false
|
collections) as c on false
|
||||||
{includeJoin:raw}
|
""";
|
||||||
""");
|
|
||||||
|
|
||||||
if (limit.AfterID != null)
|
return _database.Query<ILibraryItem>(sql, new()
|
||||||
{
|
{
|
||||||
ILibraryItem reference = await Get(limit.AfterID.Value);
|
{ "s", typeof(Show) },
|
||||||
Filter<ILibraryItem>? keysetFilter = RepositoryHelper.KeysetPaginate(sort, reference, !limit.Reverse);
|
{ "m", typeof(Movie) },
|
||||||
filter = Filter.And(filter, keysetFilter);
|
{ "c", typeof(Collection) }
|
||||||
}
|
},
|
||||||
if (filter != null)
|
items =>
|
||||||
query += ProcessFilter(filter, config);
|
{
|
||||||
query += $"order by {ProcessSort(sort, limit.Reverse, config):raw}";
|
if (items[0] is Show show && show.Id != 0)
|
||||||
query += $"limit {limit.Limit}";
|
return show;
|
||||||
|
if (items[1] is Movie movie && movie.Id != 0)
|
||||||
Type[] types = config.Select(x => x.Value)
|
return movie;
|
||||||
.Concat(includeConfig.Select(x => x.Value))
|
if (items[2] is Collection collection && collection.Id != 0)
|
||||||
.ToArray();
|
return collection;
|
||||||
IEnumerable<ILibraryItem> data = await query.QueryAsync<ILibraryItem>(types, items =>
|
throw new InvalidDataException();
|
||||||
{
|
},
|
||||||
if (items[0] is Show show && show.Id != 0)
|
(id) => Get(id),
|
||||||
return mapIncludes(show, items.Skip(3));
|
include, filter, sort, limit
|
||||||
if (items[1] is Movie movie && movie.Id != 0)
|
);
|
||||||
return mapIncludes(movie, items.Skip(3));
|
|
||||||
if (items[2] is Collection collection && collection.Id != 0)
|
|
||||||
return mapIncludes(collection, items.Skip(3));
|
|
||||||
throw new InvalidDataException();
|
|
||||||
});
|
|
||||||
if (limit.Reverse)
|
|
||||||
data = data.Reverse();
|
|
||||||
return data.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<int> GetCount(Filter<ILibraryItem>? filter = null)
|
public Task<int> GetCount(Filter<ILibraryItem>? filter = null)
|
||||||
|
@ -265,7 +265,7 @@ namespace Kyoo.Core.Controllers
|
|||||||
public virtual Task<ICollection<T>> GetAll(Filter<T>? filter = null,
|
public virtual Task<ICollection<T>> GetAll(Filter<T>? filter = null,
|
||||||
Sort<T>? sort = default,
|
Sort<T>? sort = default,
|
||||||
Include<T>? include = default,
|
Include<T>? include = default,
|
||||||
Pagination limit = default)
|
Pagination? limit = default)
|
||||||
{
|
{
|
||||||
return ApplyFilters(Database.Set<T>(), filter, sort, limit, include);
|
return ApplyFilters(Database.Set<T>(), filter, sort, limit, include);
|
||||||
}
|
}
|
||||||
@ -282,11 +282,12 @@ namespace Kyoo.Core.Controllers
|
|||||||
protected async Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
|
protected async Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
|
||||||
Filter<T>? filter = null,
|
Filter<T>? filter = null,
|
||||||
Sort<T>? sort = default,
|
Sort<T>? sort = default,
|
||||||
Pagination limit = default,
|
Pagination? limit = default,
|
||||||
Include<T>? include = default)
|
Include<T>? include = default)
|
||||||
{
|
{
|
||||||
query = AddIncludes(query, include);
|
query = AddIncludes(query, include);
|
||||||
query = Sort(query, sort);
|
query = Sort(query, sort);
|
||||||
|
limit ??= new();
|
||||||
|
|
||||||
if (limit.AfterID != null)
|
if (limit.AfterID != null)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user