Implement a base repository for dapper

This commit is contained in:
Zoe Roux 2023-11-26 23:05:39 +01:00
parent 179b79c926
commit ba37786038
5 changed files with 207 additions and 225 deletions

View File

@ -174,19 +174,19 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
"pe".* -- Episode as pe
pe.* -- Episode as pe
from
episodes as "pe"
where
"pe".show_id = "this".show_id
and ("pe".absolute_number < "this".absolute_number
or "pe".season_number < "this".season_number
or ("pe".season_number = "this".season_number
pe.show_id = "this".show_id
and (pe.absolute_number < "this".absolute_number
or pe.season_number < "this".season_number
or (pe.season_number = "this".season_number
and e.episode_number < "this".episode_number))
order by
"pe".absolute_number desc,
"pe".season_number desc,
"pe".episode_number desc
pe.absolute_number desc,
pe.season_number desc,
pe.episode_number desc
limit 1
"""
)]
@ -210,19 +210,19 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
"ne".* -- Episode as ne
ne.* -- Episode as ne
from
episodes as "ne"
where
"ne".show_id = "this".show_id
and ("ne".absolute_number > "this".absolute_number
or "ne".season_number > "this".season_number
or ("ne".season_number = "this".season_number
ne.show_id = "this".show_id
and (ne.absolute_number > "this".absolute_number
or ne.season_number > "this".season_number
or (ne.season_number = "this".season_number
and e.episode_number > "this".episode_number))
order by
"ne".absolute_number,
"ne".season_number,
"ne".episode_number
ne.absolute_number,
ne.season_number,
ne.episode_number
limit 1
"""
)]

View File

@ -158,7 +158,7 @@ namespace Kyoo.Abstractions.Models
// language=PostgreSQL
Sql = """
select
"fe".* -- Episode as fe
fe.* -- Episode as fe
from (
select
e.*,
@ -166,7 +166,7 @@ namespace Kyoo.Abstractions.Models
from
episodes as e) as "fe"
where
"fe".number <= 1
fe.number <= 1
""",
On = "show_id = \"this\".id"
)]

View File

@ -48,11 +48,9 @@ public static class DapperHelper
return $"coalesce({string.Join(", ", keys)})";
}
public static string ProcessSort<T>(Sort<T>? sort, bool reverse, Dictionary<string, Type> config, bool recurse = false)
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),
@ -78,7 +76,7 @@ public static class DapperHelper
Dictionary<string, Type> retConfig = new();
StringBuilder join = new();
foreach (Include<T>.Metadata metadata in include.Metadatas)
foreach (Include.Metadata metadata in include.Metadatas)
{
relation++;
switch (metadata)
@ -103,7 +101,7 @@ public static class DapperHelper
}
}
T Map(T item, IEnumerable<object> relations)
T Map(T item, IEnumerable<object?> relations)
{
foreach ((string name, object? value) in include.Fields.Zip(relations))
{
@ -177,7 +175,7 @@ public static class DapperHelper
this IDbConnection db,
FormattableString command,
Dictionary<string, Type> config,
Func<object?[], T> mapper,
Func<List<object?>, T> mapper,
Func<int, Task<T>> get,
Include<T>? include,
Filter<T>? filter,
@ -205,7 +203,8 @@ public static class DapperHelper
}
if (filter != null)
query += ProcessFilter(filter, config);
query += $"\norder by {ProcessSort(sort, limit.Reverse, config):raw}";
if (sort != null)
query += $"\norder by {ProcessSort(sort, limit.Reverse, config):raw}";
query += $"\nlimit {limit.Limit}";
// Build query and prepare to do the query/projections
@ -260,7 +259,7 @@ public static class DapperHelper
thumbs.Thumbnail = items[++i] as Image;
thumbs.Logo = items[++i] as Image;
}
return mapIncludes(mapper(nItems.ToArray()), nItems.Skip(config.Count));
return mapIncludes(mapper(nItems), nItems.Skip(config.Count));
},
ParametersDictionary.LoadFrom(cmd),
splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id"))
@ -269,4 +268,40 @@ public static class DapperHelper
data = data.Reverse();
return data.ToList();
}
public static async Task<T?> QuerySingle<T>(
this IDbConnection db,
FormattableString command,
Dictionary<string, Type> config,
Func<List<object?>, T> mapper,
Include<T>? include,
Filter<T>? filter,
Sort<T>? sort = null)
where T : class, IResource, IQuery
{
ICollection<T> ret = await db.Query<T>(command, config, mapper, null!, include, filter, sort, new Pagination(1));
return ret.FirstOrDefault();
}
public static async Task<int> Count<T>(
this IDbConnection db,
FormattableString command,
Dictionary<string, Type> config,
Filter<T>? filter)
where T : class, IResource
{
InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command);
if (filter != null)
query += ProcessFilter(filter, config);
IDapperSqlCommand cmd = query.Build();
// language=postgreSQL
string sql = $"select count(*) from ({cmd.Sql})";
return await db.ExecuteAsync(
sql,
ParametersDictionary.LoadFrom(cmd)
);
}
}

View File

@ -18,87 +18,156 @@
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.Data.Common;
using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
namespace Kyoo.Core.Controllers;
public class DapperRepository<T> : IRepository<T>
public abstract class DapperRepository<T> : IRepository<T>
where T : class, IResource, IQuery
{
public Type RepositoryType => typeof(T);
protected abstract FormattableString Sql { get; }
protected abstract Dictionary<string, Type> Config { get; }
protected abstract T Mapper(List<object?> items);
protected DbConnection Database { get; init; }
public DapperRepository(DbConnection database)
{
Database = database;
}
/// <inheritdoc/>
public virtual async Task<T> Get(int id, Include<T>? include = default)
{
T? ret = await GetOrDefault(id, include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<T> Get(string slug, Include<T>? include = default)
{
T? ret = await GetOrDefault(slug, include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<T> Get(Filter<T> filter,
Include<T>? include = default)
{
T? ret = await GetOrDefault(filter, include: include);
if (ret == null)
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
return ret;
}
/// <inheritdoc />
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();
}
/// <inheritdoc />
public Task<T?> GetOrDefault(int id, Include<T>? include = null)
{
throw new NotImplementedException();
return Database.QuerySingle<T>(
Sql,
Config,
Mapper,
include,
new Filter<T>.Eq(nameof(IResource.Id), id)
);
}
/// <inheritdoc />
public Task<T?> GetOrDefault(string slug, Include<T>? include = null)
{
throw new NotImplementedException();
return Database.QuerySingle<T>(
Sql,
Config,
Mapper,
include,
new Filter<T>.Eq(nameof(IResource.Slug), slug)
);
}
/// <inheritdoc />
public Task<T?> GetOrDefault(Filter<T>? filter, Include<T>? include = null, Sort<T>? sortBy = null)
{
throw new NotImplementedException();
return Database.QuerySingle<T>(
Sql,
Config,
Mapper,
include,
filter,
sortBy
);
}
public Task<ICollection<T>> Search(string query, Include<T>? include = null)
/// <inheritdoc />
public Task<ICollection<T>> GetAll(Filter<T>? filter = default,
Sort<T>? sort = default,
Include<T>? include = default,
Pagination? limit = default)
{
throw new NotImplementedException();
return Database.Query<T>(
Sql,
Config,
Mapper,
(id) => Get(id),
include,
filter,
sort ?? new Sort<T>.Default(),
limit ?? new()
);
}
/// <inheritdoc />
public Task<int> GetCount(Filter<T>? filter = null)
{
return Database.Count(
Sql,
Config,
filter
);
}
/// <inheritdoc />
public Task<ICollection<T>> Search(string query, Include<T>? include = null) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> Create(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> CreateIfNotExists(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(int id) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(string slug) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task DeleteAll(Filter<T> filter) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> Edit(T edited) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> Patch(int id, Func<T, Task<bool>> patch) => throw new NotImplementedException();
}

View File

@ -18,157 +18,63 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Dapper;
using InterpolatedSql.Dapper;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
namespace Kyoo.Core.Controllers
{
/// <summary>
/// A local repository to handle library items.
/// </summary>
public class LibraryItemRepository : IRepository<ILibraryItem>
public class LibraryItemRepository : DapperRepository<ILibraryItem>
{
private readonly DbConnection _database;
// language=PostgreSQL
protected override FormattableString Sql => $"""
select
s.*, -- Show as s
m.*,
c.*
/* includes */
from
shows as s
full outer join (
select
* -- Movie
from
movies) as m on false
full outer join(
select
* -- Collection
from
collections) as c on false
""";
public Type RepositoryType => typeof(ILibraryItem);
protected override Dictionary<string, Type> Config => new()
{
{ "s", typeof(Show) },
{ "m", typeof(Movie) },
{ "c", typeof(Collection) }
};
protected override ILibraryItem Mapper(List<object?> items)
{
if (items[0] is Show show && show.Id != 0)
return show;
if (items[1] is Movie movie && movie.Id != 0)
return movie;
if (items[2] is Collection collection && collection.Id != 0)
return collection;
throw new InvalidDataException();
}
public LibraryItemRepository(DbConnection database)
{
_database = database;
}
/// <inheritdoc/>
public virtual async Task<ILibraryItem> Get(int id, Include<ILibraryItem>? include = default)
{
ILibraryItem? ret = await GetOrDefault(id, include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the id {id}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<ILibraryItem> Get(string slug, Include<ILibraryItem>? include = default)
{
ILibraryItem? ret = await GetOrDefault(slug, include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the slug {slug}");
return ret;
}
/// <inheritdoc/>
public virtual async Task<ILibraryItem> Get(Filter<ILibraryItem> filter,
Include<ILibraryItem>? include = default)
{
ILibraryItem? ret = await GetOrDefault(filter, include: include);
if (ret == null)
throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the given predicate.");
return ret;
}
public Task<ILibraryItem?> GetOrDefault(int id, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task<ILibraryItem?> GetOrDefault(string slug, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task<ILibraryItem?> GetOrDefault(Filter<ILibraryItem>? filter, Include<ILibraryItem>? include = default,
Sort<ILibraryItem>? sortBy = default)
{
throw new NotImplementedException();
}
public Task<ICollection<ILibraryItem>> GetAll(
Filter<ILibraryItem>? filter = null,
Sort<ILibraryItem>? sort = default,
Include<ILibraryItem>? include = default,
Pagination? limit = default)
{
// language=PostgreSQL
FormattableString sql = $"""
select
s.*, -- Show as s
m.*,
c.*
/* includes */
from
shows as s
full outer join (
select
* -- Movie
from
movies) as m on false
full outer join (
select
* -- Collection
from
collections) as c on false
""";
return _database.Query<ILibraryItem>(sql, new()
{
{ "s", typeof(Show) },
{ "m", typeof(Movie) },
{ "c", typeof(Collection) }
},
items =>
{
if (items[0] is Show show && show.Id != 0)
return show;
if (items[1] is Movie movie && movie.Id != 0)
return movie;
if (items[2] is Collection collection && collection.Id != 0)
return collection;
throw new InvalidDataException();
},
(id) => Get(id),
include, filter, sort, limit ?? new()
);
}
public Task<int> GetCount(Filter<ILibraryItem>? filter = null)
{
throw new NotImplementedException();
}
public Task<ICollection<ILibraryItem>> FromIds(IList<int> ids, Include<ILibraryItem>? include = null)
{
throw new NotImplementedException();
}
public Task DeleteAll(Filter<ILibraryItem> filter)
{
throw new NotImplementedException();
}
/// <inheritdoc />
public async Task<ICollection<ILibraryItem>> Search(string query, Include<ILibraryItem>? include = default)
{
throw new NotImplementedException();
// return await Sort(
// AddIncludes(_database.LibraryItems, include)
// .Where(_database.Like<LibraryItem>(x => x.Name, $"%{query}%"))
// )
// .Take(20)
// .ToListAsync();
}
: base(database)
{ }
public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
Expression<Func<Collection, bool>> selector,
@ -193,33 +99,5 @@ namespace Kyoo.Core.Controllers
// limit,
// include);
}
/// <inheritdoc />
public Task<ILibraryItem> Create(ILibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<ILibraryItem> CreateIfNotExists(ILibraryItem obj)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<ILibraryItem> Edit(ILibraryItem edited)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task<ILibraryItem> Patch(int id, Func<ILibraryItem, Task<bool>> patch)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task Delete(int id)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task Delete(string slug)
=> throw new InvalidOperationException();
/// <inheritdoc />
public Task Delete(ILibraryItem obj)
=> throw new InvalidOperationException();
}
}