diff --git a/back/src/Kyoo.Abstractions/Models/Attributes/SqlFirstColumnAttribute.cs b/back/src/Kyoo.Abstractions/Models/Attributes/SqlFirstColumnAttribute.cs
new file mode 100644
index 00000000..e420a1f3
--- /dev/null
+++ b/back/src/Kyoo.Abstractions/Models/Attributes/SqlFirstColumnAttribute.cs
@@ -0,0 +1,37 @@
+// 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;
+using Kyoo.Utils;
+
+namespace Kyoo.Abstractions.Models.Attributes;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class SqlFirstColumnAttribute : Attribute
+{
+ ///
+ /// The name of the first column of the element. Used to split multiples
+ /// items on a single sql query. If not specified, it defaults to "Id".
+ ///
+ public string Name { get; set; }
+
+ public SqlFirstColumnAttribute(string name)
+ {
+ Name = name.ToSnakeCase();
+ }
+}
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
index aef11180..1d0a728e 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Episode.cs
@@ -244,7 +244,11 @@ namespace Kyoo.Abstractions.Models
/// Metadata of what an user as started/planned to watch.
///
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
- [LoadableRelation] public EpisodeWatchStatus? WatchStatus { get; set; }
+ [LoadableRelation(
+ Sql = "episode_watch_status",
+ On = "episode_id = \"this\".id and \"relation\".user_id = [current_user]"
+ )]
+ public EpisodeWatchStatus? WatchStatus { get; set; }
// There is a global query filter to filter by user so we just need to do single.
private EpisodeWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
index 820f9470..057601b7 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
@@ -20,6 +20,7 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
+using Kyoo.Abstractions.Models.Attributes;
using Newtonsoft.Json;
namespace Kyoo.Abstractions.Models
@@ -47,6 +48,7 @@ namespace Kyoo.Abstractions.Models
}
[TypeConverter(typeof(ImageConvertor))]
+ [SqlFirstColumn(nameof(Source))]
public class Image
{
///
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
index 14d9e035..819bc4fd 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Movie.cs
@@ -152,7 +152,11 @@ namespace Kyoo.Abstractions.Models
/// Metadata of what an user as started/planned to watch.
///
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
- [LoadableRelation] public MovieWatchStatus? WatchStatus { get; set; }
+ [LoadableRelation(
+ Sql = "movie_watch_status",
+ On = "movie_id = \"this\".id and \"relation\".user_id = [current_user]"
+ )]
+ public MovieWatchStatus? WatchStatus { get; set; }
// There is a global query filter to filter by user so we just need to do single.
private MovieWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
index 61906b4d..bb5311fd 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/Show.cs
@@ -193,7 +193,11 @@ namespace Kyoo.Abstractions.Models
/// Metadata of what an user as started/planned to watch.
///
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
- [LoadableRelation] public ShowWatchStatus? WatchStatus { get; set; }
+ [LoadableRelation(
+ Sql = "show_watch_status",
+ On = "show_id = \"this\".id and \"relation\".user_id = [current_user]"
+ )]
+ public ShowWatchStatus? WatchStatus { get; set; }
// There is a global query filter to filter by user so we just need to do single.
private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
diff --git a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
index 36c4f6fc..ffb74638 100644
--- a/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
+++ b/back/src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
@@ -53,6 +53,7 @@ namespace Kyoo.Abstractions.Models
///
/// Metadata of what an user as started/planned to watch.
///
+ [SqlFirstColumn(nameof(UserId))]
public class MovieWatchStatus : IAddedDate
{
///
@@ -105,6 +106,7 @@ namespace Kyoo.Abstractions.Models
public int? WatchedPercent { get; set; }
}
+ [SqlFirstColumn(nameof(UserId))]
public class EpisodeWatchStatus : IAddedDate
{
///
@@ -157,6 +159,7 @@ namespace Kyoo.Abstractions.Models
public int? WatchedPercent { get; set; }
}
+ [SqlFirstColumn(nameof(UserId))]
public class ShowWatchStatus : IAddedDate
{
///
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
index 72d11011..c6b1aaf6 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
@@ -28,15 +28,35 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Dapper;
using InterpolatedSql.Dapper;
+using InterpolatedSql.Dapper.SqlBuilders;
using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
+using Kyoo.Abstractions.Models.Attributes;
using Kyoo.Abstractions.Models.Utils;
+using Kyoo.Authentication;
using Kyoo.Utils;
+using Microsoft.AspNetCore.Http;
namespace Kyoo.Core.Controllers;
public static class DapperHelper
{
+ public static SqlBuilder ProcessVariables(SqlBuilder sql, SqlVariableContext context)
+ {
+ int start = 0;
+ while ((start = sql.IndexOf("[", start, false)) != -1)
+ {
+ int end = sql.IndexOf("]", start, false);
+ if (end == -1)
+ throw new ArgumentException("Invalid sql variable substitue (missing ])");
+ string var = sql.Format[(start + 1)..end];
+ sql.Remove(start, end - start + 1);
+ sql.Insert(start, $"{context.ReadVar(var)}");
+ }
+
+ return sql;
+ }
+
public static string Property(string key, Dictionary config)
{
if (key == "kind")
@@ -95,10 +115,12 @@ public static class DapperHelper
string owner = config.First(x => x.Value == declaring).Key;
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
sql = sql.Replace("\"this\"", owner);
- on = on?.Replace("\"this\"", owner);
+ on = on?.Replace("\"this\"", owner)?.Replace("\"relation\"", $"r{relation}");
+ if (sql.Any(char.IsWhiteSpace))
+ sql = $"({sql})";
types.Add(type);
projection.AppendLine($", r{relation}.*");
- join.Append($"\nleft join{lateral} ({sql}) as r{relation} on r{relation}.{on}");
+ join.Append($"\nleft join{lateral} {sql} as r{relation} on r{relation}.{on}");
break;
case Include.ProjectedRelation:
continue;
@@ -197,13 +219,14 @@ public static class DapperHelper
Dictionary config,
Func, T> mapper,
Func> get,
+ SqlVariableContext context,
Include? include,
Filter? filter,
Sort? sort,
Pagination? limit)
where T : class, IResource, IQuery
{
- InterpolatedSql.Dapper.SqlBuilders.SqlBuilder query = new(db, command);
+ SqlBuilder query = new(db, command);
// Include handling
include ??= new();
@@ -227,6 +250,8 @@ public static class DapperHelper
if (limit != null)
query += $"\nlimit {limit.Limit}";
+ ProcessVariables(query, context);
+
// Build query and prepare to do the query/projections
IDapperSqlCommand cmd = query.Build();
string sql = cmd.Sql;
@@ -280,7 +305,7 @@ public static class DapperHelper
return mapIncludes(mapper(nItems), nItems.Skip(config.Count));
},
ParametersDictionary.LoadFrom(cmd),
- splitOn: string.Join(',', types.Select(x => x == typeof(Image) ? "source" : "id"))
+ splitOn: string.Join(',', types.Select(x => x.GetCustomAttribute()?.Name ?? "id"))
);
if (limit?.Reverse == true)
data = data.Reverse();
@@ -292,6 +317,7 @@ public static class DapperHelper
FormattableString command,
Dictionary config,
Func, T> mapper,
+ SqlVariableContext context,
Include? include,
Filter? filter,
Sort? sort = null,
@@ -303,6 +329,7 @@ public static class DapperHelper
config,
mapper,
get: null!,
+ context,
include,
filter,
sort,
@@ -315,6 +342,7 @@ public static class DapperHelper
this IDbConnection db,
FormattableString command,
Dictionary config,
+ SqlVariableContext context,
Filter? filter)
where T : class, IResource
{
@@ -322,7 +350,7 @@ public static class DapperHelper
if (filter != null)
query += ProcessFilter(filter, config);
-
+ ProcessVariables(query, context);
IDapperSqlCommand cmd = query.Build();
// language=postgreSQL
@@ -334,3 +362,22 @@ public static class DapperHelper
);
}
}
+
+public class SqlVariableContext
+{
+ private readonly IHttpContextAccessor _accessor;
+
+ public SqlVariableContext(IHttpContextAccessor accessor)
+ {
+ _accessor = accessor;
+ }
+
+ public object? ReadVar(string var)
+ {
+ return var switch
+ {
+ "current_user" => _accessor.HttpContext?.User.GetId(),
+ _ => throw new ArgumentException($"Invalid sql variable name: {var}")
+ };
+ }
+}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
index 27c30c11..7806fe65 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
@@ -25,7 +25,6 @@ using Kyoo.Abstractions.Controllers;
using Kyoo.Abstractions.Models;
using Kyoo.Abstractions.Models.Exceptions;
using Kyoo.Abstractions.Models.Utils;
-using static Kyoo.Core.Controllers.DapperHelper;
namespace Kyoo.Core.Controllers;
@@ -42,9 +41,13 @@ public abstract class DapperRepository : IRepository
protected DbConnection Database { get; init; }
- public DapperRepository(DbConnection database)
+ protected SqlVariableContext Context { get; init; }
+
+
+ public DapperRepository(DbConnection database, SqlVariableContext context)
{
Database = database;
+ Context = context;
}
///
@@ -83,6 +86,7 @@ public abstract class DapperRepository : IRepository
Config,
Mapper,
(id) => Get(id),
+ Context,
include,
Filter.Or(ids.Select(x => new Filter.Eq("id", x)).ToArray()),
sort: null,
@@ -99,6 +103,7 @@ public abstract class DapperRepository : IRepository
Sql,
Config,
Mapper,
+ Context,
include,
new Filter.Eq(nameof(IResource.Id), id)
);
@@ -113,6 +118,7 @@ public abstract class DapperRepository : IRepository
Sql,
Config,
Mapper,
+ Context,
include,
filter: null,
new Sort.Random()
@@ -122,6 +128,7 @@ public abstract class DapperRepository : IRepository
Sql,
Config,
Mapper,
+ Context,
include,
new Filter.Eq(nameof(IResource.Slug), slug)
);
@@ -137,6 +144,7 @@ public abstract class DapperRepository : IRepository
Sql,
Config,
Mapper,
+ Context,
include,
filter,
sortBy
@@ -154,6 +162,7 @@ public abstract class DapperRepository : IRepository
Config,
Mapper,
(id) => Get(id),
+ Context,
include,
filter,
sort ?? new Sort.Default(),
@@ -167,6 +176,7 @@ public abstract class DapperRepository : IRepository
return Database.Count(
Sql,
Config,
+ Context,
filter
);
}
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
index 0a438961..be6272c1 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/LibraryItemRepository.cs
@@ -76,8 +76,8 @@ namespace Kyoo.Core.Controllers
throw new InvalidDataException();
}
- public LibraryItemRepository(DbConnection database)
- : base(database)
+ public LibraryItemRepository(DbConnection database, SqlVariableContext context)
+ : base(database, context)
{ }
public async Task> GetAllOfCollection(
@@ -118,6 +118,7 @@ namespace Kyoo.Core.Controllers
},
Mapper,
(id) => Get(id),
+ Context,
include,
filter,
sort ?? new Sort.Default(),
diff --git a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
index 7224d3ff..1ed9fad0 100644
--- a/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
+++ b/back/src/Kyoo.Core/Controllers/Repositories/NewsRepository.cs
@@ -60,8 +60,8 @@ namespace Kyoo.Core.Controllers
throw new InvalidDataException();
}
- public NewsRepository(DbConnection database)
- : base(database)
+ public NewsRepository(DbConnection database, SqlVariableContext context)
+ : base(database, context)
{ }
}
}
diff --git a/back/src/Kyoo.Core/CoreModule.cs b/back/src/Kyoo.Core/CoreModule.cs
index 71035c07..0339c6ac 100644
--- a/back/src/Kyoo.Core/CoreModule.cs
+++ b/back/src/Kyoo.Core/CoreModule.cs
@@ -68,6 +68,7 @@ namespace Kyoo.Core
builder.RegisterRepository();
builder.RegisterRepository();
builder.RegisterType().As().AsSelf().InstancePerLifetimeScope();
+ builder.RegisterType().InstancePerLifetimeScope();
}
///