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

View File

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

View File

@ -48,11 +48,9 @@ public static class DapperHelper
return $"coalesce({string.Join(", ", keys)})"; 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 where T : IQuery
{ {
sort ??= new Sort<T>.Default();
string ret = sort switch string ret = sort switch
{ {
Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true), Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
@ -78,7 +76,7 @@ public static class DapperHelper
Dictionary<string, Type> retConfig = new(); Dictionary<string, Type> retConfig = new();
StringBuilder join = new(); StringBuilder join = new();
foreach (Include<T>.Metadata metadata in include.Metadatas) foreach (Include.Metadata metadata in include.Metadatas)
{ {
relation++; relation++;
switch (metadata) 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)) foreach ((string name, object? value) in include.Fields.Zip(relations))
{ {
@ -177,7 +175,7 @@ public static class DapperHelper
this IDbConnection db, this IDbConnection db,
FormattableString command, FormattableString command,
Dictionary<string, Type> config, Dictionary<string, Type> config,
Func<object?[], T> mapper, Func<List<object?>, T> mapper,
Func<int, Task<T>> get, Func<int, Task<T>> get,
Include<T>? include, Include<T>? include,
Filter<T>? filter, Filter<T>? filter,
@ -205,7 +203,8 @@ public static class DapperHelper
} }
if (filter != null) if (filter != null)
query += ProcessFilter(filter, config); 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}"; query += $"\nlimit {limit.Limit}";
// Build query and prepare to do the query/projections // Build query and prepare to do the query/projections
@ -260,7 +259,7 @@ public static class DapperHelper
thumbs.Thumbnail = items[++i] as Image; thumbs.Thumbnail = items[++i] as Image;
thumbs.Logo = 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), ParametersDictionary.LoadFrom(cmd),
splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id")) splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id"))
@ -269,4 +268,40 @@ public static class DapperHelper
data = data.Reverse(); data = data.Reverse();
return data.ToList(); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.Data.Common;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
namespace Kyoo.Core.Controllers; namespace Kyoo.Core.Controllers;
public class DapperRepository<T> : IRepository<T> public abstract class DapperRepository<T> : IRepository<T>
where T : class, IResource, IQuery where T : class, IResource, IQuery
{ {
public Type RepositoryType => typeof(T); 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) public Task<ICollection<T>> FromIds(IList<int> ids, Include<T>? include = null)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }
public Task<T> Get(int id, Include<T>? include = null) /// <inheritdoc />
{
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) 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) 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) 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(); public Task<T> Create(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> CreateIfNotExists(T obj) => throw new NotImplementedException(); public Task<T> CreateIfNotExists(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(int id) => throw new NotImplementedException(); public Task Delete(int id) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(string slug) => throw new NotImplementedException(); public Task Delete(string slug) => throw new NotImplementedException();
/// <inheritdoc />
public Task Delete(T obj) => throw new NotImplementedException(); public Task Delete(T obj) => throw new NotImplementedException();
/// <inheritdoc />
public Task DeleteAll(Filter<T> filter) => throw new NotImplementedException(); public Task DeleteAll(Filter<T> filter) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> Edit(T edited) => throw new NotImplementedException(); public Task<T> Edit(T edited) => throw new NotImplementedException();
/// <inheritdoc />
public Task<T> Patch(int id, Func<T, Task<bool>> patch) => throw new NotImplementedException(); public Task<T> Patch(int id, Func<T, Task<bool>> patch) => throw new NotImplementedException();
} }

View File

@ -18,157 +18,63 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common; using System.Data.Common;
using System.IO; using System.IO;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper;
using InterpolatedSql.Dapper;
using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils; using Kyoo.Abstractions.Models.Utils;
using Kyoo.Utils;
namespace Kyoo.Core.Controllers namespace Kyoo.Core.Controllers
{ {
/// <summary> /// <summary>
/// A local repository to handle library items. /// A local repository to handle library items.
/// </summary> /// </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) public LibraryItemRepository(DbConnection database)
{ : base(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();
}
public async Task<ICollection<ILibraryItem>> GetAllOfCollection( public async Task<ICollection<ILibraryItem>> GetAllOfCollection(
Expression<Func<Collection, bool>> selector, Expression<Func<Collection, bool>> selector,
@ -193,33 +99,5 @@ namespace Kyoo.Core.Controllers
// limit, // limit,
// include); // 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();
} }
} }