Rework filters completly

This commit is contained in:
Zoe Roux 2023-11-22 20:02:35 +01:00
parent e8351e960d
commit e9aaa184cf
30 changed files with 259 additions and 376 deletions

View File

@ -61,11 +61,11 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Get the first resource that match the predicate. /// Get the first resource that match the predicate.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter the resource.</param> /// <param name="filter">A predicate to filter the resource.</param>
/// <param name="include">The related fields to include.</param> /// <param name="include">The related fields to include.</param>
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception> /// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T> Get(Expression<Func<T, bool>> where, Include<T>? include = default); Task<T> Get(Filter<T> filter, Include<T>? include = default);
/// <summary> /// <summary>
/// Get a resource from it's ID or null if it is not found. /// Get a resource from it's ID or null if it is not found.
@ -86,11 +86,11 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Get the first resource that match the predicate or null if it is not found. /// Get the first resource that match the predicate or null if it is not found.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter the resource.</param> /// <param name="filter">A predicate to filter the resource.</param>
/// <param name="include">The related fields to include.</param> /// <param name="include">The related fields to include.</param>
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param> /// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
/// <returns>The resource found</returns> /// <returns>The resource found</returns>
Task<T?> GetOrDefault(Expression<Func<T, bool>> where, Task<T?> GetOrDefault(Filter<T>? filter,
Include<T>? include = default, Include<T>? include = default,
Sort<T>? sortBy = default); Sort<T>? sortBy = default);
@ -105,22 +105,22 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Get every resources that match all filters /// Get every resources that match all filters
/// </summary> /// </summary>
/// <param name="where">A filter predicate</param> /// <param name="filter">A filter predicate</param>
/// <param name="sort">Sort information about the query (sort by, sort order)</param> /// <param name="sort">Sort information about the query (sort by, sort order)</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <param name="include">The related fields to include.</param> /// <param name="include">The related fields to include.</param>
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
/// <returns>A list of resources that match every filters</returns> /// <returns>A list of resources that match every filters</returns>
Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null, Task<ICollection<T>> GetAll(Filter<T>? filter = null,
Sort<T>? sort = default, Sort<T>? sort = default,
Pagination? limit = default, Include<T>? include = default,
Include<T>? include = 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.
/// </summary> /// </summary>
/// <param name="where">A filter predicate</param> /// <param name="filter">A filter predicate</param>
/// <returns>How many resources matched that filter</returns> /// <returns>How many resources matched that filter</returns>
Task<int> GetCount(Expression<Func<T, bool>>? where = null); Task<int> GetCount(Filter<T>? filter = null);
/// <summary> /// <summary>
/// Map a list of ids to a list of items (keep the order). /// Map a list of ids to a list of items (keep the order).
@ -217,9 +217,9 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Delete all resources that match the predicate. /// Delete all resources that match the predicate.
/// </summary> /// </summary>
/// <param name="where">A predicate to filter resources to delete. Every resource that match this will be deleted.</param> /// <param name="filter">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task DeleteAll(Expression<Func<T, bool>> where); Task DeleteAll(Filter<T> filter);
/// <summary> /// <summary>
/// Called when a resource has been edited. /// Called when a resource has been edited.

View File

@ -0,0 +1,69 @@
// 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.Linq;
using System.Linq.Expressions;
namespace Kyoo.Abstractions.Models.Utils;
public abstract record Filter
{
public static Filter<T>? And<T>(params Filter<T>?[] filters)
{
return filters
.Where(x => x != null)
.Aggregate((Filter<T>?)null, (acc, filter) =>
{
if (acc == null)
return filter;
return new Filter<T>.And(acc, filter!);
});
}
}
public abstract record Filter<T> : Filter
{
public record And(Filter<T> first, Filter<T> second) : Filter<T>;
public record Or(Filter<T> first, Filter<T> second) : Filter<T>;
public record Not(Filter<T> filter) : Filter<T>;
public record Eq(string property, object value) : Filter<T>;
public record Ne<T2>(string property, T2 value) : Filter<T>;
public record Gt<T2>(string property, T2 value) : Filter<T>;
public record Ge<T2>(string property, T2 value) : Filter<T>;
public record Lt<T2>(string property, T2 value) : Filter<T>;
public record Le<T2>(string property, T2 value) : Filter<T>;
public record Has<T2>(string property, T2 value) : Filter<T>;
public record In(string property, object[] value) : Filter<T>;
public record Lambda(Expression<Func<T, bool>> lambda) : Filter<T>;
public static Filter<T> From(string filter)
{
}
}

View File

@ -99,13 +99,14 @@ namespace Kyoo.Abstractions.Models.Utils
/// identifier.Matcher&lt;Season&gt;(x => x.ShowID, x => x.Show.Slug) /// identifier.Matcher&lt;Season&gt;(x => x.ShowID, x => x.Show.Slug)
/// </code> /// </code>
/// </example> /// </example>
public Expression<Func<T, bool>> Matcher<T>(Expression<Func<T, int>> idGetter, public Filter<T> Matcher<T>(Expression<Func<T, int>> idGetter,
Expression<Func<T, string>> slugGetter) Expression<Func<T, string>> slugGetter)
{ {
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters); Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
return new Filter<T>.Lambda(lambda);
} }
/// <summary> /// <summary>
@ -117,13 +118,14 @@ namespace Kyoo.Abstractions.Models.Utils
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param> /// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
/// <typeparam name="T">The type to match against this identifier.</typeparam> /// <typeparam name="T">The type to match against this identifier.</typeparam>
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns> /// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
public Expression<Func<T, bool>> Matcher<T>(Expression<Func<T, int?>> idGetter, public Filter<T> Matcher<T>(Expression<Func<T, int?>> idGetter,
Expression<Func<T, string>> slugGetter) Expression<Func<T, string>> slugGetter)
{ {
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug); ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self); BinaryExpression equal = Expression.Equal(_id.HasValue ? idGetter.Body : slugGetter.Body, self);
ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters; ICollection<ParameterExpression> parameters = _id.HasValue ? idGetter.Parameters : slugGetter.Parameters;
return Expression.Lambda<Func<T, bool>>(equal, parameters); Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
return new Filter<T>.Lambda(lambda);
} }
/// <summary> /// <summary>
@ -142,13 +144,21 @@ namespace Kyoo.Abstractions.Models.Utils
} }
/// <summary> /// <summary>
/// Return an expression that return true if this <see cref="Identifier"/> match a given resource. /// Return a filter to get this <see cref="Identifier"/> match a given resource.
/// </summary> /// </summary>
/// <typeparam name="T">The type of resource to match against.</typeparam> /// <typeparam name="T">The type of resource to match against.</typeparam>
/// <returns> /// <returns>
/// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise. /// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise.
/// </returns> /// </returns>
public Expression<Func<T, bool>> IsSame<T>() public Filter<T> IsSame<T>()
where T : IResource
{
return _id.HasValue
? new Filter<T>.Eq("Id", _id.Value)
: new Filter<T>.Eq("Slug", _slug!);
}
private Expression<Func<T, bool>> _IsSameExpression<T>()
where T : IResource where T : IResource
{ {
return _id.HasValue return _id.HasValue
@ -163,7 +173,7 @@ namespace Kyoo.Abstractions.Models.Utils
/// <typeparam name="T">The type that contain the list to check.</typeparam> /// <typeparam name="T">The type that contain the list to check.</typeparam>
/// <typeparam name="T2">The type of resource to check this identifier against.</typeparam> /// <typeparam name="T2">The type of resource to check this identifier against.</typeparam>
/// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns> /// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns>
public Expression<Func<T, bool>> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>>> listGetter) public Filter<T> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>?>> listGetter)
where T2 : IResource where T2 : IResource
{ {
MethodInfo method = typeof(Enumerable) MethodInfo method = typeof(Enumerable)
@ -171,8 +181,9 @@ namespace Kyoo.Abstractions.Models.Utils
.Where(x => x.Name == nameof(Enumerable.Any)) .Where(x => x.Name == nameof(Enumerable.Any))
.FirstOrDefault(x => x.GetParameters().Length == 2)! .FirstOrDefault(x => x.GetParameters().Length == 2)!
.MakeGenericMethod(typeof(T2)); .MakeGenericMethod(typeof(T2));
MethodCallExpression call = Expression.Call(null, method, listGetter.Body, IsSame<T2>()); MethodCallExpression call = Expression.Call(null, method, listGetter.Body, _IsSameExpression<T2>());
return Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters); Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(call, listGetter.Parameters);
return new Filter<T>.Lambda(lambda);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -21,7 +21,7 @@ namespace Kyoo.Abstractions.Controllers
/// <summary> /// <summary>
/// Information about the pagination. How many items should be displayed and where to start. /// Information about the pagination. How many items should be displayed and where to start.
/// </summary> /// </summary>
public class Pagination public struct Pagination
{ {
/// <summary> /// <summary>
/// The count of items to return. /// The count of items to return.

View File

@ -1,125 +0,0 @@
// 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.Reflection;
namespace Kyoo.Utils
{
/// <summary>
/// Static class containing MethodOf calls.
/// </summary>
public static class MethodOfUtils
{
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf(Action action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the action.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T>(Action<T> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the action.</typeparam>
/// <typeparam name="T2">The second parameter of the action.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2>(Action<T, T2> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the action.</typeparam>
/// <typeparam name="T2">The second parameter of the action.</typeparam>
/// <typeparam name="T3">The third parameter of the action.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3>(Action<T, T2, T3> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The return type of function.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T>(Func<T> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the function.</typeparam>
/// <typeparam name="T2">The return type of function.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2>(Func<T, T2> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the function.</typeparam>
/// <typeparam name="T2">The second parameter of the function.</typeparam>
/// <typeparam name="T3">The return type of function.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3>(Func<T, T2, T3> action)
{
return action.Method;
}
/// <summary>
/// Get a MethodInfo from a direct method.
/// </summary>
/// <param name="action">The method (without any arguments or return value.</param>
/// <typeparam name="T">The first parameter of the function.</typeparam>
/// <typeparam name="T2">The second parameter of the function.</typeparam>
/// <typeparam name="T3">The third parameter of the function.</typeparam>
/// <typeparam name="T4">The return type of function.</typeparam>
/// <returns>The <see cref="MethodInfo"/> of the given method</returns>
public static MethodInfo MethodOf<T, T2, T3, T4>(Func<T, T2, T3, T4> action)
{
return action.Method;
}
}
}

View File

@ -1,52 +0,0 @@
// 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.Threading.Tasks;
namespace Kyoo.Utils
{
/// <summary>
/// A class containing helper method for tasks.
/// </summary>
public static class TaskUtils
{
/// <summary>
/// Run a method after the execution of the task.
/// </summary>
/// <param name="task">The task to wait.</param>
/// <param name="then">
/// The method to run after the task finish. This will only be run if the task finished successfully.
/// </param>
/// <typeparam name="T">The type of the item in the task.</typeparam>
/// <returns>A continuation task wrapping the initial task and adding a continuation method.</returns>
/// <exception cref="TaskCanceledException">The source task has been canceled.</exception>
public static Task<T> Then<T>(this Task<T> task, Action<T> then)
{
return task.ContinueWith(x =>
{
if (x.IsFaulted)
x.Exception!.InnerException!.ReThrow();
if (x.IsCanceled)
throw new TaskCanceledException();
then(x.Result);
return x.Result;
}, TaskContinuationOptions.ExecuteSynchronously);
}
}
}

View File

@ -342,18 +342,5 @@ namespace Kyoo.Utils
return string.Empty; return string.Empty;
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}")); return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
} }
/// <summary>
/// Rethrow the exception without modifying the stack trace.
/// This is similar to the <c>rethrow;</c> code but is useful when the exception is not in a catch block.
/// </summary>
/// <param name="ex">The exception to rethrow.</param>
[System.Diagnostics.CodeAnalysis.DoesNotReturn]
public static void ReThrow(this Exception ex)
{
if (ex == null)
throw new ArgumentNullException(nameof(ex));
ExceptionDispatchInfo.Capture(ex).Throw();
}
} }
} }

View File

@ -96,7 +96,7 @@ namespace Kyoo.Authentication.Views
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request) public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
{ {
User? user = await _users.GetOrDefault(x => x.Username == request.Username); User? user = await _users.GetOrDefault(new Filter<User>.Eq(nameof(Abstractions.Models.User.Username), request.Username));
if (user == null || !BCryptNet.Verify(request.Password, user.Password)) if (user == null || !BCryptNet.Verify(request.Password, user.Password))
return Forbid(new RequestError("The user and password does not match.")); return Forbid(new RequestError("The user and password does not match."));
@ -126,7 +126,7 @@ namespace Kyoo.Authentication.Views
User user = request.ToUser(); User user = request.ToUser();
user.Permissions = _permissions.NewUser; user.Permissions = _permissions.NewUser;
// If no users exists, the new one will be an admin. Give it every permissions. // If no users exists, the new one will be an admin. Give it every permissions.
if (await _users.GetOrDefault(where: x => true) == null) if (await _users.GetOrDefault(1) == null)
user.Permissions = PermissionOption.Admin; user.Permissions = PermissionOption.Admin;
try try
{ {

View File

@ -53,7 +53,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Collection>> Search(string query, Include<Collection>? include = default) public override async Task<ICollection<Collection>> Search(string query, Include<Collection>? include = default)
{ {
return await AddIncludes(_database.Collections, include) return await AddIncludes(_database.Collections, include)
.Where(_database.Like<Collection>(x => x.Name + " " + x.Slug, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }

View File

@ -79,7 +79,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Episode>> Search(string query, Include<Episode>? include = default) public override async Task<ICollection<Episode>> Search(string query, Include<Episode>? include = default)
{ {
return await AddIncludes(_database.Episodes, include) return await AddIncludes(_database.Episodes, include)
.Where(_database.Like<Episode>(x => x.Name!, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -92,8 +92,8 @@ namespace Kyoo.Core.Controllers
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => await _database.SaveChangesAsync(() =>
obj is { SeasonNumber: not null, EpisodeNumber: not null } obj is { SeasonNumber: not null, EpisodeNumber: not null }
? Get(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber && x.EpisodeNumber == obj.EpisodeNumber) ? _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber && x.EpisodeNumber == obj.EpisodeNumber)
: Get(x => x.ShowId == obj.ShowId && x.AbsoluteNumber == obj.AbsoluteNumber)); : _database.Episodes.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.AbsoluteNumber == obj.AbsoluteNumber));
await IRepository<Episode>.OnResourceCreated(obj); await IRepository<Episode>.OnResourceCreated(obj);
return obj; return obj;
} }

View File

@ -29,10 +29,8 @@ 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.Attributes;
using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils; using Kyoo.Utils;
@ -72,11 +70,10 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<ILibraryItem> Get( public virtual async Task<ILibraryItem> Get(Filter<ILibraryItem> filter,
Expression<Func<ILibraryItem, bool>> where,
Include<ILibraryItem>? include = default) Include<ILibraryItem>? include = default)
{ {
ILibraryItem? ret = await GetOrDefault(where, include: include); ILibraryItem? ret = await GetOrDefault(filter, include: include);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the given predicate."); throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the given predicate.");
return ret; return ret;
@ -92,7 +89,8 @@ namespace Kyoo.Core.Controllers
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<ILibraryItem?> GetOrDefault(Expression<Func<ILibraryItem, bool>> where, Include<ILibraryItem>? include = null, Sort<ILibraryItem>? sortBy = null) public Task<ILibraryItem?> GetOrDefault(Filter<ILibraryItem>? filter, Include<ILibraryItem>? include = default,
Sort<ILibraryItem>? sortBy = default)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -108,9 +106,11 @@ namespace Kyoo.Core.Controllers
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) public static string ProcessSort<T>(Sort<T>? sort, Dictionary<string, Type> config, bool recurse = false)
where T : IQuery where T : IQuery
{ {
sort ??= new Sort<T>.Default();
string ret = sort switch string ret = sort switch
{ {
Sort<T>.Default(var value) => ProcessSort(value, config, true), Sort<T>.Default(var value) => ProcessSort(value, config, true),
@ -188,12 +188,24 @@ namespace Kyoo.Core.Controllers
return $"{prefix}*" + projStr; return $"{prefix}*" + projStr;
} }
public async Task<ICollection<ILibraryItem>> GetAll( public static string ProcessFilter<T>(Filter<T> filter, Dictionary<string, Type> config)
Expression<Func<ILibraryItem, bool>>? where = null,
Sort<ILibraryItem>? sort = null,
Pagination? limit = null,
Include<ILibraryItem>? include = null)
{ {
return filter switch
{
Filter<T>.And(var first, var second) => $"({ProcessFilter(first, config)} and {ProcessFilter(second, config)})",
Filter<T>.Or(var first, var second) => $"({ProcessFilter(first, config)} or {ProcessFilter(second, config)})",
Filter<T>.Not(var inner) => $"(not {ProcessFilter(inner, config)})",
Filter<T>.Eq(var property, var value) => $"({_Property(property, config)} = {value})",
};
}
public async Task<ICollection<ILibraryItem>> GetAll(Filter<ILibraryItem>? filter = null,
Sort<ILibraryItem>? sort = default,
Include<ILibraryItem>? include = default,
Pagination limit = default)
{
include ??= new();
Dictionary<string, Type> config = new() Dictionary<string, Type> config = new()
{ {
{ "s", typeof(Show) }, { "s", typeof(Show) },
@ -203,7 +215,7 @@ namespace Kyoo.Core.Controllers
var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config); var (includeConfig, includeJoin, mapIncludes) = ProcessInclude(include, config);
// language=PostgreSQL // language=PostgreSQL
IDapperSqlCommand query = _database.SqlBuilder($""" var query = _database.SqlBuilder($"""
select select
{ExpendProjections<Show>("s", include):raw}, {ExpendProjections<Show>("s", include):raw},
m.*, m.*,
@ -224,7 +236,10 @@ namespace Kyoo.Core.Controllers
{includeJoin:raw} {includeJoin:raw}
order by {ProcessSort(sort, config):raw} order by {ProcessSort(sort, config):raw}
limit {limit.Limit} limit {limit.Limit}
""").Build(); """);
if (filter != null)
query += $"where {ProcessFilter(filter, config):raw}";
Type[] types = config.Select(x => x.Value) Type[] types = config.Select(x => x.Value)
.Concat(includeConfig.Select(x => x.Value)) .Concat(includeConfig.Select(x => x.Value))
@ -242,7 +257,7 @@ namespace Kyoo.Core.Controllers
return data.ToList(); return data.ToList();
} }
public Task<int> GetCount(Expression<Func<ILibraryItem, bool>>? where = null) public Task<int> GetCount(Filter<ILibraryItem>? filter = null)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
@ -252,7 +267,7 @@ namespace Kyoo.Core.Controllers
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task DeleteAll(Expression<Func<ILibraryItem, bool>> where) public Task DeleteAll(Filter<ILibraryItem> filter)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

View File

@ -113,6 +113,11 @@ namespace Kyoo.Core.Controllers
return _Sort(query, sortBy, false).ThenBy(x => x.Id); return _Sort(query, sortBy, false).ThenBy(x => x.Id);
} }
protected static Expression<Func<T, bool>> ParseFilter(Filter<T>? filter)
{
throw new NotImplementedException();
}
private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression( private static Func<Expression, Expression, BinaryExpression> _GetComparisonExpression(
bool desc, bool desc,
bool next, bool next,
@ -297,9 +302,9 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<T> Get(Expression<Func<T, bool>> where, Include<T>? include = default) public virtual async Task<T> Get(Filter<T> filter, Include<T>? include = default)
{ {
T? ret = await GetOrDefault(where, include: include); T? ret = await GetOrDefault(filter, include: include);
if (ret == null) if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate."); throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret; return ret;
@ -326,7 +331,7 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc /> /// <inheritdoc />
public virtual Task<T?> GetOrDefault(Expression<Func<T, bool>> where, public virtual Task<T?> GetOrDefault(Filter<T>? filter,
Include<T>? include = default, Include<T>? include = default,
Sort<T>? sortBy = default) Sort<T>? sortBy = default)
{ {
@ -334,7 +339,7 @@ namespace Kyoo.Core.Controllers
AddIncludes(Database.Set<T>(), include), AddIncludes(Database.Set<T>(), include),
sortBy sortBy
) )
.FirstOrDefaultAsync(where); .FirstOrDefaultAsync(ParseFilter(filter));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -353,12 +358,12 @@ namespace Kyoo.Core.Controllers
public abstract Task<ICollection<T>> Search(string query, Include<T>? include = default); public abstract Task<ICollection<T>> Search(string query, Include<T>? include = default);
/// <inheritdoc/> /// <inheritdoc/>
public virtual Task<ICollection<T>> GetAll(Expression<Func<T, bool>>? where = null, public virtual Task<ICollection<T>> GetAll(Filter<T>? filter = null,
Sort<T>? sort = default, Sort<T>? sort = default,
Pagination? limit = default, Include<T>? include = default,
Include<T>? include = default) Pagination limit = default)
{ {
return ApplyFilters(Database.Set<T>(), where, sort, limit, include); return ApplyFilters(Database.Set<T>(), ParseFilter(filter), sort, limit, include);
} }
/// <summary> /// <summary>
@ -373,7 +378,7 @@ namespace Kyoo.Core.Controllers
protected async Task<ICollection<T>> ApplyFilters(IQueryable<T> query, protected async Task<ICollection<T>> ApplyFilters(IQueryable<T> query,
Expression<Func<T, bool>>? where = null, Expression<Func<T, bool>>? where = 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);
@ -381,25 +386,25 @@ namespace Kyoo.Core.Controllers
if (where != null) if (where != null)
query = query.Where(where); query = query.Where(where);
if (limit?.AfterID != null) if (limit.AfterID != null)
{ {
T reference = await Get(limit.AfterID.Value); T reference = await Get(limit.AfterID.Value);
query = query.Where(KeysetPaginate(sort, reference, !limit.Reverse)); query = query.Where(KeysetPaginate(sort, reference, !limit.Reverse));
} }
if (limit?.Reverse == true) if (limit.Reverse)
query = query.Reverse(); query = query.Reverse();
if (limit?.Limit > 0) if (limit.Limit > 0)
query = query.Take(limit.Limit); query = query.Take(limit.Limit);
return await query.ToListAsync(); return await query.ToListAsync();
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual Task<int> GetCount(Expression<Func<T, bool>>? where = null) public virtual Task<int> GetCount(Filter<T>? filter = null)
{ {
IQueryable<T> query = Database.Set<T>(); IQueryable<T> query = Database.Set<T>();
if (where != null) if (filter != null)
query = query.Where(where); query = query.Where(ParseFilter(filter));
return query.CountAsync(); return query.CountAsync();
} }
@ -559,9 +564,9 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc/> /// <inheritdoc/>
public async Task DeleteAll(Expression<Func<T, bool>> where) public async Task DeleteAll(Filter<T> filter)
{ {
foreach (T resource in await GetAll(where)) foreach (T resource in await GetAll(filter))
await Delete(resource); await Delete(resource);
} }
} }

View File

@ -69,7 +69,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Movie>> Search(string query, Include<Movie>? include = default) public override async Task<ICollection<Movie>> Search(string query, Include<Movie>? include = default)
{ {
return await AddIncludes(_database.Movies, include) return await AddIncludes(_database.Movies, include)
.Where(_database.Like<Movie>(x => x.Name + " " + x.Slug, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }

View File

@ -63,7 +63,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<People>> Search(string query, Include<People>? include = default) public override async Task<ICollection<People>> Search(string query, Include<People>? include = default)
{ {
return await AddIncludes(_database.People, include) return await AddIncludes(_database.People, include)
.Where(_database.Like<People>(x => x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }

View File

@ -74,7 +74,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Season>> Search(string query, Include<Season>? include = default) public override async Task<ICollection<Season>> Search(string query, Include<Season>? include = default)
{ {
return await AddIncludes(_database.Seasons, include) return await AddIncludes(_database.Seasons, include)
.Where(_database.Like<Season>(x => x.Name!, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name!, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -85,7 +85,9 @@ namespace Kyoo.Core.Controllers
await base.Create(obj); await base.Create(obj);
obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug; obj.ShowSlug = _database.Shows.First(x => x.Id == obj.ShowId).Slug;
_database.Entry(obj).State = EntityState.Added; _database.Entry(obj).State = EntityState.Added;
await _database.SaveChangesAsync(() => Get(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber)); await _database.SaveChangesAsync(() =>
_database.Seasons.FirstOrDefaultAsync(x => x.ShowId == obj.ShowId && x.SeasonNumber == obj.SeasonNumber)
);
await IRepository<Season>.OnResourceCreated(obj); await IRepository<Season>.OnResourceCreated(obj);
return obj; return obj;
} }

View File

@ -70,7 +70,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Show>> Search(string query, Include<Show>? include = default) public override async Task<ICollection<Show>> Search(string query, Include<Show>? include = default)
{ {
return await AddIncludes(_database.Shows, include) return await AddIncludes(_database.Shows, include)
.Where(_database.Like<Show>(x => x.Name + " " + x.Slug, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -88,8 +88,6 @@ namespace Kyoo.Core.Controllers
/// <inheritdoc /> /// <inheritdoc />
protected override async Task Validate(Show resource) protected override async Task Validate(Show resource)
{ {
resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource); await base.Validate(resource);
if (resource.Studio != null) if (resource.Studio != null)
{ {

View File

@ -53,7 +53,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<Studio>> Search(string query, Include<Studio>? include = default) public override async Task<ICollection<Studio>> Search(string query, Include<Studio>? include = default)
{ {
return await AddIncludes(_database.Studios, include) return await AddIncludes(_database.Studios, include)
.Where(_database.Like<Studio>(x => x.Name, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Name, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }
@ -68,13 +68,6 @@ namespace Kyoo.Core.Controllers
return obj; return obj;
} }
/// <inheritdoc />
protected override async Task Validate(Studio resource)
{
resource.Slug ??= Utility.ToSlug(resource.Name);
await base.Validate(resource);
}
/// <inheritdoc /> /// <inheritdoc />
public override async Task Delete(Studio obj) public override async Task Delete(Studio obj)
{ {

View File

@ -52,7 +52,7 @@ namespace Kyoo.Core.Controllers
public override async Task<ICollection<User>> Search(string query, Include<User>? include = default) public override async Task<ICollection<User>> Search(string query, Include<User>? include = default)
{ {
return await AddIncludes(_database.Users, include) return await AddIncludes(_database.Users, include)
.Where(_database.Like<User>(x => x.Username, $"%{query}%")) .Where(x => EF.Functions.ILike(x.Username, $"%{query}%"))
.Take(20) .Take(20)
.ToListAsync(); .ToListAsync();
} }

View File

@ -18,7 +18,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
@ -86,16 +85,16 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Get the number of resources that match the filters. /// Get the number of resources that match the filters.
/// </remarks> /// </remarks>
/// <param name="where">A list of filters to respect.</param> /// <param name="filter">A list of filters to respect.</param>
/// <returns>How many resources matched that filter.</returns> /// <returns>How many resources matched that filter.</returns>
/// <response code="400">Invalid filters.</response> /// <response code="400">Invalid filters.</response>
[HttpGet("count")] [HttpGet("count")]
[PartialPermission(Kind.Read)] [PartialPermission(Kind.Read)]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<int>> GetCount([FromQuery] Dictionary<string, string> where) public async Task<ActionResult<int>> GetCount([FromQuery] Filter<T> filter)
{ {
return await Repository.GetCount(ApiHelper.ParseWhere<T>(where)); return await Repository.GetCount(filter);
} }
/// <summary> /// <summary>
@ -105,7 +104,7 @@ namespace Kyoo.Core.Api
/// Get all resources that match the given filter. /// Get all resources that match the given filter.
/// </remarks> /// </remarks>
/// <param name="sortBy">Sort information about the query (sort by, sort order).</param> /// <param name="sortBy">Sort information about the query (sort by, sort order).</param>
/// <param name="where">Filter the returned items.</param> /// <param name="filter">Filter the returned items.</param>
/// <param name="pagination">How many items per page should be returned, where should the page start...</param> /// <param name="pagination">How many items per page should be returned, where should the page start...</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A list of resources that match every filters.</returns> /// <returns>A list of resources that match every filters.</returns>
@ -116,15 +115,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<ActionResult<Page<T>>> GetAll( public async Task<ActionResult<Page<T>>> GetAll(
[FromQuery] Sort<T> sortBy, [FromQuery] Sort<T> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<T>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<T>? fields) [FromQuery] Include<T>? fields)
{ {
ICollection<T> resources = await Repository.GetAll( ICollection<T> resources = await Repository.GetAll(
ApiHelper.ParseWhere<T>(where), filter,
sortBy, sortBy,
pagination, fields,
fields pagination
); );
return Page(resources, pagination.Limit); return Page(resources, pagination.Limit);
@ -231,20 +230,19 @@ namespace Kyoo.Core.Api
/// <remarks> /// <remarks>
/// Delete all items matching the given filters. If no filter is specified, delete all items. /// Delete all items matching the given filters. If no filter is specified, delete all items.
/// </remarks> /// </remarks>
/// <param name="where">The list of filters.</param> /// <param name="filter">The list of filters.</param>
/// <returns>The item(s) has successfully been deleted.</returns> /// <returns>The item(s) has successfully been deleted.</returns>
/// <response code="400">One or multiple filters are invalid.</response> /// <response code="400">One or multiple filters are invalid.</response>
[HttpDelete] [HttpDelete]
[PartialPermission(Kind.Delete)] [PartialPermission(Kind.Delete)]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
public async Task<IActionResult> Delete([FromQuery] Dictionary<string, string> where) public async Task<IActionResult> Delete([FromQuery] Filter<T> filter)
{ {
Expression<Func<T, bool>>? w = ApiHelper.ParseWhere<T>(where); if (filter == null)
if (w == null)
return BadRequest(new RequestError("Incule a filter to delete items, all items won't be deleted.")); return BadRequest(new RequestError("Incule a filter to delete items, all items won't be deleted."));
await Repository.DeleteAll(w); await Repository.DeleteAll(filter);
return NoContent(); return NoContent();
} }
} }

View File

@ -65,7 +65,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Studio"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Studio"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param> /// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of shows to return.</param> /// <param name="pagination">The number of shows to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of shows.</returns> /// <returns>A page of shows.</returns>
@ -79,15 +79,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier, public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] Sort<Show> sortBy, [FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Show>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Show> fields) [FromQuery] Include<Show> fields)
{ {
ICollection<Show> resources = await _libraryManager.Shows.GetAll( ICollection<Show> resources = await _libraryManager.Shows.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)), Filter.And(filter, identifier.Matcher<Show>(x => x.StudioId, x => x.Studio!.Slug)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Studios.GetOrDefault(identifier.IsSame<Studio>()) == null) if (!resources.Any() && await _libraryManager.Studios.GetOrDefault(identifier.IsSame<Studio>()) == null)

View File

@ -126,7 +126,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort items by.</param> /// <param name="sortBy">A key to sort items by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of items to return.</param> /// <param name="pagination">The number of items to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of items.</returns> /// <returns>A page of items.</returns>
@ -140,21 +140,21 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<ILibraryItem>>> GetItems(Identifier identifier, public async Task<ActionResult<Page<ILibraryItem>>> GetItems(Identifier identifier,
[FromQuery] Sort<ILibraryItem> sortBy, [FromQuery] Sort<ILibraryItem> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<ILibraryItem>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<ILibraryItem>? fields) [FromQuery] Include<ILibraryItem>? fields)
{ {
ICollection<ILibraryItem> resources = await _items.GetAllOfCollection( // ICollection<ILibraryItem> resources = await _items.GetAllOfCollection(
identifier.IsSame<Collection>(), // identifier.IsSame<Collection>(),
ApiHelper.ParseWhere<ILibraryItem>(where), // filter,
sortBy == new Sort<ILibraryItem>.Default() ? new Sort<ILibraryItem>.By(nameof(Movie.AirDate)) : sortBy, // sortBy == new Sort<ILibraryItem>.Default() ? new Sort<ILibraryItem>.By(nameof(Movie.AirDate)) : sortBy,
pagination, // pagination,
fields // fields
); // );
if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null) // if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
return NotFound(); return NotFound();
return Page(resources, pagination.Limit); // return Page(resources, pagination.Limit);
} }
/// <summary> /// <summary>
@ -165,9 +165,9 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort shows by.</param> /// <param name="sortBy">A key to sort shows by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of shows to return.</param> /// <param name="pagination">The number of shows to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The additional fields to include in the result.</param>
/// <returns>A page of shows.</returns> /// <returns>A page of shows.</returns>
/// <response code="400">The filters or the sort parameters are invalid.</response> /// <response code="400">The filters or the sort parameters are invalid.</response>
/// <response code="404">No collection with the given ID could be found.</response> /// <response code="404">No collection with the given ID could be found.</response>
@ -179,15 +179,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier, public async Task<ActionResult<Page<Show>>> GetShows(Identifier identifier,
[FromQuery] Sort<Show> sortBy, [FromQuery] Sort<Show> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Show>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Show>? fields) [FromQuery] Include<Show>? fields)
{ {
ICollection<Show> resources = await _libraryManager.Shows.GetAll( ICollection<Show> resources = await _libraryManager.Shows.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Show, Collection>(x => x.Collections!)), Filter.And(filter, identifier.IsContainedIn<Show, Collection>(x => x.Collections)),
sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy, sortBy == new Sort<Show>.Default() ? new Sort<Show>.By(x => x.AirDate) : sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null) if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)
@ -203,7 +203,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Collection"/>.</param>
/// <param name="sortBy">A key to sort movies by.</param> /// <param name="sortBy">A key to sort movies by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of movies to return.</param> /// <param name="pagination">The number of movies to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of movies.</returns> /// <returns>A page of movies.</returns>
@ -217,15 +217,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Movie>>> GetMovies(Identifier identifier, public async Task<ActionResult<Page<Movie>>> GetMovies(Identifier identifier,
[FromQuery] Sort<Movie> sortBy, [FromQuery] Sort<Movie> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Movie>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Movie>? fields) [FromQuery] Include<Movie>? fields)
{ {
ICollection<Movie> resources = await _libraryManager.Movies.GetAll( ICollection<Movie> resources = await _libraryManager.Movies.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Movie, Collection>(x => x.Collections!)), Filter.And(filter, identifier.IsContainedIn<Movie, Collection>(x => x.Collections)),
sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy, sortBy == new Sort<Movie>.Default() ? new Sort<Movie>.By(x => x.AirDate) : sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null) if (!resources.Any() && await _libraryManager.Collections.GetOrDefault(identifier.IsSame<Collection>()) == null)

View File

@ -120,7 +120,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param> /// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of collections to return.</param> /// <param name="pagination">The number of collections to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of collections.</returns> /// <returns>A page of collections.</returns>
@ -134,15 +134,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier, public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] Sort<Collection> sortBy, [FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Collection>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Collection> fields) [FromQuery] Include<Collection> fields)
{ {
ICollection<Collection> resources = await _libraryManager.Collections.GetAll( ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Movie>(x => x.Movies!)), Filter.And(filter, identifier.IsContainedIn<Collection, Movie>(x => x.Movies)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Movies.GetOrDefault(identifier.IsSame<Movie>()) == null) if (!resources.Any() && await _libraryManager.Movies.GetOrDefault(identifier.IsSame<Movie>()) == null)

View File

@ -16,13 +16,10 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Attributes; using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Permissions;
using Kyoo.Core.Controllers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using static Kyoo.Abstractions.Models.Utils.Constants; using static Kyoo.Abstractions.Models.Utils.Constants;
@ -37,25 +34,10 @@ namespace Kyoo.Core.Api
[ResourceView] [ResourceView]
[PartialPermission("LibraryItem")] [PartialPermission("LibraryItem")]
[ApiDefinition("News", Group = ResourcesGroup)] [ApiDefinition("News", Group = ResourcesGroup)]
public class NewsApi : BaseApi public class NewsApi : CrudThumbsApi<News>
{ {
private readonly NewsRepository _news; public NewsApi(IRepository<News> news, IThumbnailsManager thumbs)
: base(news, thumbs)
public NewsApi(NewsRepository news) { }
{
_news = news;
}
public async Task<ActionResult<Page<News>>> GetAll(
[FromQuery] Dictionary<string, string> where,
[FromQuery] Pagination pagination)
{
ICollection<News> resources = await _news.GetAll(
ApiHelper.ParseWhere<News>(where),
limit: pagination
);
return Page(resources, pagination.Limit);
}
} }
} }

View File

@ -67,7 +67,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Season"/>.</param>
/// <param name="sortBy">A key to sort episodes by.</param> /// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of episodes to return.</param> /// <param name="pagination">The number of episodes to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of episodes.</returns> /// <returns>A page of episodes.</returns>
@ -81,15 +81,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier, public async Task<ActionResult<Page<Episode>>> GetEpisode(Identifier identifier,
[FromQuery] Sort<Episode> sortBy, [FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Episode>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Episode> fields) [FromQuery] Include<Episode> fields)
{ {
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll( ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)), Filter.And(filter, identifier.Matcher<Episode>(x => x.SeasonId, x => x.Season!.Slug)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null) if (!resources.Any() && await _libraryManager.Seasons.GetOrDefault(identifier.IsSame<Season>()) == null)

View File

@ -67,7 +67,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort seasons by.</param> /// <param name="sortBy">A key to sort seasons by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of seasons to return.</param> /// <param name="pagination">The number of seasons to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of seasons.</returns> /// <returns>A page of seasons.</returns>
@ -81,15 +81,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Season>>> GetSeasons(Identifier identifier, public async Task<ActionResult<Page<Season>>> GetSeasons(Identifier identifier,
[FromQuery] Sort<Season> sortBy, [FromQuery] Sort<Season> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Season>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Season> fields) [FromQuery] Include<Season> fields)
{ {
ICollection<Season> resources = await _libraryManager.Seasons.GetAll( ICollection<Season> resources = await _libraryManager.Seasons.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)), Filter.And(filter, identifier.Matcher<Season>(x => x.ShowId, x => x.Show!.Slug)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
@ -105,7 +105,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort episodes by.</param> /// <param name="sortBy">A key to sort episodes by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of episodes to return.</param> /// <param name="pagination">The number of episodes to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of episodes.</returns> /// <returns>A page of episodes.</returns>
@ -119,15 +119,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier, public async Task<ActionResult<Page<Episode>>> GetEpisodes(Identifier identifier,
[FromQuery] Sort<Episode> sortBy, [FromQuery] Sort<Episode> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Episode>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Episode> fields) [FromQuery] Include<Episode> fields)
{ {
ICollection<Episode> resources = await _libraryManager.Episodes.GetAll( ICollection<Episode> resources = await _libraryManager.Episodes.GetAll(
ApiHelper.ParseWhere(where, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)), Filter.And(filter, identifier.Matcher<Episode>(x => x.ShowId, x => x.Show!.Slug)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)
@ -197,7 +197,7 @@ namespace Kyoo.Core.Api
/// </remarks> /// </remarks>
/// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param> /// <param name="identifier">The ID or slug of the <see cref="Show"/>.</param>
/// <param name="sortBy">A key to sort collections by.</param> /// <param name="sortBy">A key to sort collections by.</param>
/// <param name="where">An optional list of filters.</param> /// <param name="filter">An optional list of filters.</param>
/// <param name="pagination">The number of collections to return.</param> /// <param name="pagination">The number of collections to return.</param>
/// <param name="fields">The aditional fields to include in the result.</param> /// <param name="fields">The aditional fields to include in the result.</param>
/// <returns>A page of collections.</returns> /// <returns>A page of collections.</returns>
@ -211,15 +211,15 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier, public async Task<ActionResult<Page<Collection>>> GetCollections(Identifier identifier,
[FromQuery] Sort<Collection> sortBy, [FromQuery] Sort<Collection> sortBy,
[FromQuery] Dictionary<string, string> where, [FromQuery] Filter<Collection>? filter,
[FromQuery] Pagination pagination, [FromQuery] Pagination pagination,
[FromQuery] Include<Collection> fields) [FromQuery] Include<Collection> fields)
{ {
ICollection<Collection> resources = await _libraryManager.Collections.GetAll( ICollection<Collection> resources = await _libraryManager.Collections.GetAll(
ApiHelper.ParseWhere(where, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)), Filter.And(filter, identifier.IsContainedIn<Collection, Show>(x => x.Shows!)),
sortBy, sortBy,
pagination, fields,
fields pagination
); );
if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null) if (!resources.Any() && await _libraryManager.Shows.GetOrDefault(identifier.IsSame<Show>()) == null)

View File

@ -541,14 +541,5 @@ namespace Kyoo.Postgresql
entry.State = EntityState.Detached; entry.State = EntityState.Detached;
} }
} }
/// <summary>
/// Perform a case insensitive like operation.
/// </summary>
/// <param name="query">An accessor to get the item that will be checked.</param>
/// <param name="format">The second operator of the like format.</param>
/// <typeparam name="T">The type of the item to query</typeparam>
/// <returns>An expression representing the like query. It can directly be passed to a where call.</returns>
public abstract Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format);
} }
} }

View File

@ -139,14 +139,5 @@ namespace Kyoo.Postgresql
{ {
return ex.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation }; return ex.InnerException is PostgresException { SqlState: PostgresErrorCodes.UniqueViolation };
} }
/// <inheritdoc />
public override Expression<Func<T, bool>> Like<T>(Expression<Func<T, string>> query, string format)
{
MethodInfo iLike = MethodOfUtils.MethodOf<string, string, bool>(EF.Functions.ILike);
MethodCallExpression call = Expression.Call(iLike, Expression.Constant(EF.Functions), query.Body, Expression.Constant(format));
return Expression.Lambda<Func<T, bool>>(call, query.Parameters);
}
} }
} }

View File

@ -1,3 +1,21 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Linq; using System.Linq;

View File

@ -89,7 +89,7 @@ namespace Kyoo.Tests.Database
} }
public IRepository<T> GetRepository<T>() public IRepository<T> GetRepository<T>()
where T : class, IResource where T : class, IResource, IQuery
{ {
return _repositories.First(x => x.RepositoryType == typeof(T)) as IRepository<T>; return _repositories.First(x => x.RepositoryType == typeof(T)) as IRepository<T>;
} }

View File

@ -29,7 +29,7 @@ using Xunit;
namespace Kyoo.Tests.Database namespace Kyoo.Tests.Database
{ {
public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable public abstract class RepositoryTests<T> : IDisposable, IAsyncDisposable
where T : class, IResource where T : class, IResource, IQuery
{ {
protected readonly RepositoryActivator Repositories; protected readonly RepositoryActivator Repositories;
private readonly IRepository<T> _repository; private readonly IRepository<T> _repository;