From e8f2a7251643171ed1ee3a2384121e14d853d618 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 17 Nov 2023 14:31:23 +0100 Subject: [PATCH] Install dapper and use it for library items --- .../Repositories/LibraryItemRepository.cs | 195 +++++++++++++----- back/src/Kyoo.Core/Kyoo.Core.csproj | 1 + .../Kyoo.Postgresql/Kyoo.Postgresql.csproj | 1 + back/src/Kyoo.Postgresql/PostgresModule.cs | 43 +++- .../Kyoo.Postgresql/Utils/JsonTypeHandler.cs | 42 ++++ .../Kyoo.Postgresql/Utils/ListTypeHandler.cs | 21 ++ .../Database/RepositoryActivator.cs | 2 +- shell.nix | 1 + 8 files changed, 248 insertions(+), 58 deletions(-) create mode 100644 back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs create mode 100644 back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs index 3085ae1b..4b3008cd 100644 --- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs +++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs @@ -18,50 +18,152 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Data.Common; +using System.IO; using System.Linq.Expressions; using System.Threading.Tasks; +using Dapper; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; +using Kyoo.Abstractions.Models.Exceptions; using Kyoo.Abstractions.Models.Utils; -using Kyoo.Postgresql; -using Microsoft.EntityFrameworkCore; namespace Kyoo.Core.Controllers { /// /// A local repository to handle library items. /// - public class LibraryItemRepository : LocalRepository + public class LibraryItemRepository : IRepository { - /// - /// The database handle - /// - private readonly DatabaseContext _database; + private readonly DbConnection _database; - /// - protected override Sort DefaultSort => new Sort.By(x => x.Name); + protected Sort DefaultSort => new Sort.By(x => x.Name); - /// - /// Create a new . - /// - /// The database instance - /// The thumbnail manager used to store images. - public LibraryItemRepository(DatabaseContext database, IThumbnailsManager thumbs) - : base(database, thumbs) + public Type RepositoryType => typeof(LibraryItem); + + public LibraryItemRepository(DbConnection database) { _database = database; } - /// - public override async Task> Search(string query, Include? include = default) + /// + public virtual async Task Get(int id, Include? include = default) { - return await Sort( - AddIncludes(_database.LibraryItems, include) - .Where(_database.Like(x => x.Name, $"%{query}%")) - ) - .Take(20) - .ToListAsync(); + LibraryItem? ret = await GetOrDefault(id, include); + if (ret == null) + throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the id {id}"); + return ret; + } + + /// + public virtual async Task Get(string slug, Include? include = default) + { + LibraryItem? ret = await GetOrDefault(slug, include); + if (ret == null) + throw new ItemNotFoundException($"No {nameof(LibraryItem)} found with the slug {slug}"); + return ret; + } + + /// + public virtual async Task Get( + Expression> where, + Include? include = default) + { + LibraryItem? ret = await GetOrDefault(where, include: include); + if (ret == null) + throw new ItemNotFoundException($"No {nameof(LibraryItem)} 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(Expression> where, Include? include = null, Sort? sortBy = null) + { + throw new NotImplementedException(); + } + + public async Task> GetAll( + Expression>? where = null, + Sort? sort = null, + Pagination? limit = null, + Include? include = null) + { + List ret = new(limit.Limit); + + // language=PostgreSQL + string sql = @" + select s.*, m.*, c.* from shows as s full outer join ( + select * from movies + ) as m on false + full outer join ( + select * from collections + ) as c on false + "; + + var data = await _database.QueryAsync(sql, new[] { typeof(Show), typeof(Movie), 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(); + }, where); + + // await using DbDataReader reader = await _database.ExecuteReaderAsync(sql); + // int kindOrdinal = reader.GetOrdinal("kind"); + // var showParser = reader.GetRowParser(typeof(Show)); + // var movieParser = reader.GetRowParser(typeof(Movie)); + // var collectionParser = reader.GetRowParser(typeof(Collection)); + // + // while (await reader.ReadAsync()) + // { + // ItemKind type = await reader.GetFieldValueAsync(kindOrdinal); + // ret.Add(type switch + // { + // ItemKind.Show => showParser(reader), + // ItemKind.Movie => movieParser(reader), + // ItemKind.Collection => collectionParser(reader), + // _ => throw new InvalidDataException(), + // }); + // } + throw new NotImplementedException(); + // return ret; + } + + public Task GetCount(Expression>? where = null) + { + throw new NotImplementedException(); + } + + public Task> FromIds(IList ids, Include? include = null) + { + throw new NotImplementedException(); + } + + public Task DeleteAll(Expression> where) + { + 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(); } public async Task> GetAllOfCollection( @@ -71,48 +173,49 @@ namespace Kyoo.Core.Controllers Pagination? limit = default, Include? include = default) { - return await ApplyFilters( - _database.LibraryItems - .Where(item => - _database.Movies - .Where(x => x.Id == -item.Id) - .Any(x => x.Collections!.AsQueryable().Any(selector)) - || _database.Shows - .Where(x => x.Id == item.Id) - .Any(x => x.Collections!.AsQueryable().Any(selector)) - ), - where, - sort, - limit, - include); + throw new NotImplementedException(); + // return await ApplyFilters( + // _database.LibraryItems + // .Where(item => + // _database.Movies + // .Where(x => x.Id == -item.Id) + // .Any(x => x.Collections!.AsQueryable().Any(selector)) + // || _database.Shows + // .Where(x => x.Id == item.Id) + // .Any(x => x.Collections!.AsQueryable().Any(selector)) + // ), + // where, + // sort, + // limit, + // include); } /// - public override Task Create(LibraryItem obj) + public Task Create(LibraryItem obj) => throw new InvalidOperationException(); /// - public override Task CreateIfNotExists(LibraryItem obj) + public Task CreateIfNotExists(LibraryItem obj) => throw new InvalidOperationException(); /// - public override Task Edit(LibraryItem edited) + public Task Edit(LibraryItem edited) => throw new InvalidOperationException(); /// - public override Task Patch(int id, Func> patch) + public Task Patch(int id, Func> patch) => throw new InvalidOperationException(); /// - public override Task Delete(int id) + public Task Delete(int id) => throw new InvalidOperationException(); /// - public override Task Delete(string slug) + public Task Delete(string slug) => throw new InvalidOperationException(); /// - public override Task Delete(LibraryItem obj) + public Task Delete(LibraryItem obj) => throw new InvalidOperationException(); } } diff --git a/back/src/Kyoo.Core/Kyoo.Core.csproj b/back/src/Kyoo.Core/Kyoo.Core.csproj index 3ad70404..1f9bb856 100644 --- a/back/src/Kyoo.Core/Kyoo.Core.csproj +++ b/back/src/Kyoo.Core/Kyoo.Core.csproj @@ -8,6 +8,7 @@ + diff --git a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj index 29710b92..b852ee67 100644 --- a/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj +++ b/back/src/Kyoo.Postgresql/Kyoo.Postgresql.csproj @@ -6,6 +6,7 @@ + diff --git a/back/src/Kyoo.Postgresql/PostgresModule.cs b/back/src/Kyoo.Postgresql/PostgresModule.cs index 1f7f46c9..3a4440e5 100644 --- a/back/src/Kyoo.Postgresql/PostgresModule.cs +++ b/back/src/Kyoo.Postgresql/PostgresModule.cs @@ -17,8 +17,14 @@ // along with Kyoo. If not, see . using System; +using System.Collections.Generic; using System.Data.Common; +using System.Text.RegularExpressions; +using Dapper; +using EFCore.NamingConventions.Internal; using Kyoo.Abstractions.Controllers; +using Kyoo.Abstractions.Models; +using Kyoo.Postgresql.Utils; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; @@ -69,29 +75,44 @@ namespace Kyoo.Postgresql using NpgsqlConnection conn = (NpgsqlConnection)context.Database.GetDbConnection(); conn.Open(); conn.ReloadTypes(); + + SqlMapper.TypeMapProvider = (type) => + { + return new CustomPropertyTypeMap(type, (type, name) => + { + string newName = Regex.Replace(name, "(^|_)([a-z])", (match) => match.Groups[2].Value.ToUpperInvariant()); + // TODO: Add images handling here (name: poster_source, newName: PosterSource) should set Poster.Source + return type.GetProperty(newName)!; + }); + }; + SqlMapper.AddTypeHandler(typeof(Dictionary), new JsonTypeHandler>()); + SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); + SqlMapper.AddTypeHandler(typeof(List), new ListTypeHandler()); } /// public void Configure(IServiceCollection services) { + DbConnectionStringBuilder builder = new() + { + ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"), + ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"), + ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"), + ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"), + ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"), + ["POOLING"] = "true", + ["MAXPOOLSIZE"] = "95", + ["TIMEOUT"] = "30" + }; + services.AddDbContext(x => { - DbConnectionStringBuilder builder = new() - { - ["USER ID"] = _configuration.GetValue("POSTGRES_USER", "KyooUser"), - ["PASSWORD"] = _configuration.GetValue("POSTGRES_PASSWORD", "KyooPassword"), - ["SERVER"] = _configuration.GetValue("POSTGRES_SERVER", "db"), - ["PORT"] = _configuration.GetValue("POSTGRES_PORT", "5432"), - ["DATABASE"] = _configuration.GetValue("POSTGRES_DB", "kyooDB"), - ["POOLING"] = "true", - ["MAXPOOLSIZE"] = "95", - ["TIMEOUT"] = "30" - }; x.UseNpgsql(builder.ConnectionString) .UseProjectables(); if (_environment.IsDevelopment()) x.EnableDetailedErrors().EnableSensitiveDataLogging(); }, ServiceLifetime.Transient); + services.AddTransient((_) => new NpgsqlConnection(builder.ConnectionString)); services.AddHealthChecks().AddDbContextCheck(); } diff --git a/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs b/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs new file mode 100644 index 00000000..1eff28a2 --- /dev/null +++ b/back/src/Kyoo.Postgresql/Utils/JsonTypeHandler.cs @@ -0,0 +1,42 @@ +// 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 . + +using System.Data; +using Newtonsoft.Json; +using Npgsql; +using NpgsqlTypes; +using static Dapper.SqlMapper; + +namespace Kyoo.Postgresql.Utils; + +public class JsonTypeHandler : TypeHandler + where T : class +{ + public override T? Parse(object value) + { + if (value is string str) + return JsonConvert.DeserializeObject(str); + return default; + } + + public override void SetValue(IDbDataParameter parameter, T? value) + { + parameter.Value = JsonConvert.SerializeObject(value); + ((NpgsqlParameter)parameter).NpgsqlDbType = NpgsqlDbType.Jsonb; + } +} diff --git a/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs b/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs new file mode 100644 index 00000000..8f20f9ba --- /dev/null +++ b/back/src/Kyoo.Postgresql/Utils/ListTypeHandler.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Dapper; + +namespace Kyoo.Postgresql.Utils; + +// See https://github.com/DapperLib/Dapper/issues/1424 +public class ListTypeHandler : SqlMapper.TypeHandler> +{ + public override List Parse(object value) + { + T[] typedValue = (T[])value; // looks like Dapper did not indicate the property type to Npgsql, so it defaults to string[] (default CLR type for text[] PostgreSQL type) + return typedValue?.ToList() ?? new(); + } + + public override void SetValue(IDbDataParameter parameter, List? value) + { + parameter.Value = value; // no need to convert to string[] in this direction + } +} diff --git a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs index 6a7a864c..08b914fe 100644 --- a/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs +++ b/back/tests/Kyoo.Tests/Database/RepositoryActivator.cs @@ -54,7 +54,7 @@ namespace Kyoo.Tests.Database MovieRepository movies = new(_NewContext(), studio, people, thumbs.Object); ShowRepository show = new(_NewContext(), studio, people, thumbs.Object); SeasonRepository season = new(_NewContext(), thumbs.Object); - LibraryItemRepository libraryItem = new(_NewContext(), thumbs.Object); + LibraryItemRepository libraryItem = new(_NewContext()); EpisodeRepository episode = new(_NewContext(), show, thumbs.Object); UserRepository user = new(_NewContext(), thumbs.Object); diff --git a/shell.nix b/shell.nix index 46e12cf1..21e7baea 100644 --- a/shell.nix +++ b/shell.nix @@ -27,6 +27,7 @@ in postgresql_15 eslint_d prettierd + pgformatter ]; RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";