Rework patch apis

This commit is contained in:
Zoe Roux 2023-12-20 21:10:38 +01:00
parent e668cdd89c
commit fabafcb78b
6 changed files with 37 additions and 20 deletions

View File

@ -187,11 +187,11 @@ namespace Kyoo.Abstractions.Controllers
/// <param name="id">The id of the resource to edit</param> /// <param name="id">The id of the resource to edit</param>
/// <param name="patch"> /// <param name="patch">
/// A method that will be called when you need to update every properties that you want to /// A method that will be called when you need to update every properties that you want to
/// persist. It can return false to abort the process via an ArgumentException /// persist.
/// </param> /// </param>
/// <exception cref="ItemNotFoundException">If the item is not found</exception> /// <exception cref="ItemNotFoundException">If the item is not found</exception>
/// <returns>The resource edited and completed by database's information (related items and so on)</returns> /// <returns>The resource edited and completed by database's information (related items and so on)</returns>
Task<T> Patch(Guid id, Func<T, Task<bool>> patch); Task<T> Patch(Guid id, Func<T, T> patch);
/// <summary> /// <summary>
/// Called when a resource has been edited. /// Called when a resource has been edited.

View File

@ -17,12 +17,30 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>. // along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System; using System;
using System.Collections.Generic;
using System.Reflection;
using Kyoo.Abstractions.Models;
using Newtonsoft.Json.Linq;
namespace Kyoo.Models; namespace Kyoo.Models;
public class PartialResource public class Patch<T> : Dictionary<string, JObject>
where T : class, IResource
{ {
public Guid? Id { get; set; } public Guid? Id => this.GetValueOrDefault(nameof(IResource.Id))?.ToObject<Guid>();
public string? Slug { get; set; } public string? Slug => this.GetValueOrDefault(nameof(IResource.Slug))?.ToObject<string>();
public T Apply(T current)
{
foreach ((string property, JObject value) in this)
{
PropertyInfo prop = typeof(T).GetProperty(
property,
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
)!;
prop.SetValue(current, value.ToObject(prop.PropertyType));
}
return current;
}
} }

View File

@ -246,7 +246,7 @@ namespace Kyoo.Authentication.Views
/// <remarks> /// <remarks>
/// Edit only provided informations about the currently authenticated user. /// Edit only provided informations about the currently authenticated user.
/// </remarks> /// </remarks>
/// <param name="user">The new data for the current user.</param> /// <param name="patch">The new data for the current user.</param>
/// <returns>The currently authenticated user after modifications.</returns> /// <returns>The currently authenticated user after modifications.</returns>
/// <response code="401">The user is not authenticated.</response> /// <response code="401">The user is not authenticated.</response>
/// <response code="403">The given access token is invalid.</response> /// <response code="403">The given access token is invalid.</response>
@ -255,14 +255,14 @@ namespace Kyoo.Authentication.Views
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
public async Task<ActionResult<User>> PatchMe(PartialResource user) public async Task<ActionResult<User>> PatchMe([FromBody] Patch<User> patch)
{ {
Guid userId = User.GetIdOrThrow(); Guid userId = User.GetIdOrThrow();
try try
{ {
if (user.Id.HasValue && user.Id != userId) if (patch.Id.HasValue && patch.Id != userId)
throw new ArgumentException("Can't edit your user id."); throw new ArgumentException("Can't edit your user id.");
return await _users.Patch(userId, TryUpdateModelAsync); return await _users.Patch(userId, patch.Apply);
} }
catch (ItemNotFoundException) catch (ItemNotFoundException)
{ {

View File

@ -217,5 +217,5 @@ public abstract class DapperRepository<T> : IRepository<T>
public Task<T> Edit(T edited) => throw new NotImplementedException(); public Task<T> Edit(T edited) => throw new NotImplementedException();
/// <inheritdoc /> /// <inheritdoc />
public Task<T> Patch(Guid id, Func<T, Task<bool>> patch) => throw new NotImplementedException(); public Task<T> Patch(Guid id, Func<T, T> patch) => throw new NotImplementedException();
} }

View File

@ -497,7 +497,7 @@ namespace Kyoo.Core.Controllers
} }
/// <inheritdoc/> /// <inheritdoc/>
public virtual async Task<T> Patch(Guid id, Func<T, Task<bool>> patch) public virtual async Task<T> Patch(Guid id, Func<T, T> patch)
{ {
bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled; bool lazyLoading = Database.ChangeTracker.LazyLoadingEnabled;
Database.ChangeTracker.LazyLoadingEnabled = false; Database.ChangeTracker.LazyLoadingEnabled = false;
@ -505,8 +505,7 @@ namespace Kyoo.Core.Controllers
{ {
T resource = await GetWithTracking(id); T resource = await GetWithTracking(id);
if (!await patch(resource)) resource = patch(resource);
throw new ArgumentException("Could not patch resource");
await Database.SaveChangesAsync(); await Database.SaveChangesAsync();
await IRepository<T>.OnResourceEdited(resource); await IRepository<T>.OnResourceEdited(resource);

View File

@ -180,7 +180,7 @@ namespace Kyoo.Core.Api
/// Edit only specified properties of an item. If the ID is specified it will be used to identify the resource. /// Edit only specified properties of an item. If the ID is specified it will be used to identify the resource.
/// If not, the slug will be used to identify it. /// If not, the slug will be used to identify it.
/// </remarks> /// </remarks>
/// <param name="resource">The resource to patch.</param> /// <param name="patch">The resource to patch.</param>
/// <returns>The edited resource.</returns> /// <returns>The edited resource.</returns>
/// <response code="400">The resource in the request body is invalid.</response> /// <response code="400">The resource in the request body is invalid.</response>
/// <response code="404">No item found with the specified ID (or slug).</response> /// <response code="404">No item found with the specified ID (or slug).</response>
@ -189,17 +189,17 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<T>> Patch([FromBody] PartialResource resource) public async Task<ActionResult<T>> Patch([FromBody] Patch<T> patch)
{ {
if (resource.Id.HasValue) if (patch.Id.HasValue)
return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync); return await Repository.Patch(patch.Id.Value, patch.Apply);
if (resource.Slug == null) if (patch.Slug == null)
throw new ArgumentException( throw new ArgumentException(
"Either the Id or the slug of the resource has to be defined to edit it." "Either the Id or the slug of the resource has to be defined to edit it."
); );
T old = await Repository.Get(resource.Slug); T old = await Repository.Get(patch.Slug);
return await Repository.Patch(old.Id, TryUpdateModelAsync); return await Repository.Patch(old.Id, patch.Apply);
} }
/// <summary> /// <summary>