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="patch">
/// 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>
/// <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>
Task<T> Patch(Guid id, Func<T, Task<bool>> patch);
Task<T> Patch(Guid id, Func<T, T> patch);
/// <summary>
/// Called when a resource has been edited.

View File

@ -17,12 +17,30 @@
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
using System;
using System.Collections.Generic;
using System.Reflection;
using Kyoo.Abstractions.Models;
using Newtonsoft.Json.Linq;
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>
/// Edit only provided informations about the currently authenticated user.
/// </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>
/// <response code="401">The user is not authenticated.</response>
/// <response code="403">The given access token is invalid.</response>
@ -255,14 +255,14 @@ namespace Kyoo.Authentication.Views
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized, 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();
try
{
if (user.Id.HasValue && user.Id != userId)
if (patch.Id.HasValue && patch.Id != userId)
throw new ArgumentException("Can't edit your user id.");
return await _users.Patch(userId, TryUpdateModelAsync);
return await _users.Patch(userId, patch.Apply);
}
catch (ItemNotFoundException)
{

View File

@ -217,5 +217,5 @@ public abstract class DapperRepository<T> : IRepository<T>
public Task<T> Edit(T edited) => throw new NotImplementedException();
/// <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/>
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;
Database.ChangeTracker.LazyLoadingEnabled = false;
@ -505,8 +505,7 @@ namespace Kyoo.Core.Controllers
{
T resource = await GetWithTracking(id);
if (!await patch(resource))
throw new ArgumentException("Could not patch resource");
resource = patch(resource);
await Database.SaveChangesAsync();
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.
/// If not, the slug will be used to identify it.
/// </remarks>
/// <param name="resource">The resource to patch.</param>
/// <param name="patch">The resource to patch.</param>
/// <returns>The edited resource.</returns>
/// <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>
@ -189,17 +189,17 @@ namespace Kyoo.Core.Api
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
[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)
return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync);
if (resource.Slug == null)
if (patch.Id.HasValue)
return await Repository.Patch(patch.Id.Value, patch.Apply);
if (patch.Slug == null)
throw new ArgumentException(
"Either the Id or the slug of the resource has to be defined to edit it."
);
T old = await Repository.Get(resource.Slug);
return await Repository.Patch(old.Id, TryUpdateModelAsync);
T old = await Repository.Get(patch.Slug);
return await Repository.Patch(old.Id, patch.Apply);
}
/// <summary>