From ba37786038e23d04cb1fec9d8d94555a0f879e3d Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 26 Nov 2023 23:05:39 +0100 Subject: [PATCH] Implement a base repository for dapper --- .../Models/Resources/Episode.cs | 32 +-- .../Models/Resources/Show.cs | 4 +- .../Controllers/Repositories/DapperHelper.cs | 51 ++++- .../Repositories/DapperRepository.cs | 143 +++++++++---- .../Repositories/LibraryItemRepository.cs | 202 ++++-------------- 5 files changed, 207 insertions(+), 225 deletions(-) diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs index de0deb0a..5bc83bd0 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs @@ -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 """ )] diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs index 4894731b..c1207961 100644 --- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs +++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs @@ -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" )] diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs index 2b8cea13..9cda9277 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs @@ -48,11 +48,9 @@ public static class DapperHelper return $"coalesce({string.Join(", ", keys)})"; } - public static string ProcessSort(Sort? sort, bool reverse, Dictionary config, bool recurse = false) + public static string ProcessSort(Sort sort, bool reverse, Dictionary config, bool recurse = false) where T : IQuery { - sort ??= new Sort.Default(); - string ret = sort switch { Sort.Default(var value) => ProcessSort(value, reverse, config, true), @@ -78,7 +76,7 @@ public static class DapperHelper Dictionary retConfig = new(); StringBuilder join = new(); - foreach (Include.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 relations) + T Map(T item, IEnumerable 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 config, - Func mapper, + Func, T> mapper, Func> get, Include? include, Filter? 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 QuerySingle( + this IDbConnection db, + FormattableString command, + Dictionary config, + Func, T> mapper, + Include? include, + Filter? filter, + Sort? sort = null) + where T : class, IResource, IQuery + { + ICollection ret = await db.Query(command, config, mapper, null!, include, filter, sort, new Pagination(1)); + return ret.FirstOrDefault(); + } + + public static async Task Count( + this IDbConnection db, + FormattableString command, + Dictionary config, + Filter? 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) + ); + } } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs index a9e4ec8b..36b1b5a9 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs @@ -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 : IRepository +public abstract class DapperRepository : IRepository where T : class, IResource, IQuery { public Type RepositoryType => typeof(T); + protected abstract FormattableString Sql { get; } + + protected abstract Dictionary Config { get; } + + protected abstract T Mapper(List items); + + protected DbConnection Database { get; init; } + + public DapperRepository(DbConnection database) + { + Database = database; + } + + /// + public virtual async Task Get(int id, Include? 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; + } + + /// + public virtual async Task Get(string slug, Include? 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; + } + + /// + public virtual async Task Get(Filter filter, + Include? 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; + } + + /// public Task> FromIds(IList ids, Include? include = null) { throw new NotImplementedException(); } - public Task Get(int id, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task Get(string slug, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task Get(Filter filter, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task> GetAll(Filter? filter = null, Sort? sort = null, Include? include = null, Pagination? limit = null) - { - throw new NotImplementedException(); - } - - public Task GetCount(Filter? filter = null) - { - throw new NotImplementedException(); - } - + /// public Task GetOrDefault(int id, Include? include = null) { - throw new NotImplementedException(); + return Database.QuerySingle( + Sql, + Config, + Mapper, + include, + new Filter.Eq(nameof(IResource.Id), id) + ); } + /// public Task GetOrDefault(string slug, Include? include = null) { - throw new NotImplementedException(); + return Database.QuerySingle( + Sql, + Config, + Mapper, + include, + new Filter.Eq(nameof(IResource.Slug), slug) + ); } + /// public Task GetOrDefault(Filter? filter, Include? include = null, Sort? sortBy = null) { - throw new NotImplementedException(); + return Database.QuerySingle( + Sql, + Config, + Mapper, + include, + filter, + sortBy + ); } - public Task> Search(string query, Include? include = null) + /// + public Task> GetAll(Filter? filter = default, + Sort? sort = default, + Include? include = default, + Pagination? limit = default) { - throw new NotImplementedException(); + return Database.Query( + Sql, + Config, + Mapper, + (id) => Get(id), + include, + filter, + sort ?? new Sort.Default(), + limit ?? new() + ); } + /// + public Task GetCount(Filter? filter = null) + { + return Database.Count( + Sql, + Config, + filter + ); + } + + /// + public Task> Search(string query, Include? include = null) => throw new NotImplementedException(); + + /// public Task Create(T obj) => throw new NotImplementedException(); + /// public Task CreateIfNotExists(T obj) => throw new NotImplementedException(); + /// public Task Delete(int id) => throw new NotImplementedException(); + /// public Task Delete(string slug) => throw new NotImplementedException(); + /// public Task Delete(T obj) => throw new NotImplementedException(); + /// public Task DeleteAll(Filter filter) => throw new NotImplementedException(); + /// public Task Edit(T edited) => throw new NotImplementedException(); + /// public Task Patch(int id, Func> patch) => throw new NotImplementedException(); } diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index aa4b2f10..25500bf2 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -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 { /// /// A local repository to handle library items. /// - public class LibraryItemRepository : IRepository + public class LibraryItemRepository : DapperRepository { - 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 Config => new() + { + { "s", typeof(Show) }, + { "m", typeof(Movie) }, + { "c", typeof(Collection) } + }; + + protected override ILibraryItem Mapper(List 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; - } - - /// - public virtual async Task Get(int id, Include? include = default) - { - ILibraryItem? ret = await GetOrDefault(id, include); - if (ret == null) - throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the id {id}"); - return ret; - } - - /// - public virtual async Task Get(string slug, Include? include = default) - { - ILibraryItem? ret = await GetOrDefault(slug, include); - if (ret == null) - throw new ItemNotFoundException($"No {nameof(ILibraryItem)} found with the slug {slug}"); - return ret; - } - - /// - public virtual async Task Get(Filter filter, - Include? 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 GetOrDefault(int id, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task GetOrDefault(string slug, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task GetOrDefault(Filter? filter, Include? include = default, - Sort? sortBy = default) - { - throw new NotImplementedException(); - } - - public Task> GetAll( - Filter? filter = null, - Sort? sort = default, - Include? 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(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 GetCount(Filter? filter = null) - { - throw new NotImplementedException(); - } - - public Task> FromIds(IList ids, Include? include = null) - { - throw new NotImplementedException(); - } - - public Task DeleteAll(Filter filter) - { - throw new NotImplementedException(); - } - - /// - public async Task> Search(string query, Include? include = default) - { - throw new NotImplementedException(); - // return await Sort( - // AddIncludes(_database.LibraryItems, include) - // .Where(_database.Like(x => x.Name, $"%{query}%")) - // ) - // .Take(20) - // .ToListAsync(); - } + : base(database) + { } public async Task> GetAllOfCollection( Expression> selector, @@ -193,33 +99,5 @@ namespace Kyoo.Core.Controllers // limit, // include); } - - /// - public Task Create(ILibraryItem obj) - => throw new InvalidOperationException(); - - /// - public Task CreateIfNotExists(ILibraryItem obj) - => throw new InvalidOperationException(); - - /// - public Task Edit(ILibraryItem edited) - => throw new InvalidOperationException(); - - /// - public Task Patch(int id, Func> patch) - => throw new InvalidOperationException(); - - /// - public Task Delete(int id) - => throw new InvalidOperationException(); - - /// - public Task Delete(string slug) - => throw new InvalidOperationException(); - - /// - public Task Delete(ILibraryItem obj) - => throw new InvalidOperationException(); } }