// 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 System.Collections.Generic; using System.Threading.Tasks; using Kyoo.Abstractions.Controllers; using Kyoo.Abstractions.Models; using Kyoo.Abstractions.Models.Permissions; using Kyoo.Abstractions.Models.Utils; using Kyoo.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Kyoo.Core.Api; /// /// A base class to handle CRUD operations on a specific resource type . /// /// The type of resource to make CRUD apis for. [ApiController] public class CrudApi : BaseApi where T : class, IResource, IQuery { /// /// The repository of the resource, used to retrieve, save and do operations on the baking store. /// protected IRepository Repository { get; } /// /// Create a new using the given repository and base url. /// /// /// The repository to use as a baking store for the type . /// public CrudApi(IRepository repository) { Repository = repository; } /// /// Get item /// /// /// Get a specific resource via it's ID or it's slug. /// /// The ID or slug of the resource to retrieve. /// The aditional fields to include in the result. /// The retrieved resource. /// A resource with the given ID or slug does not exist. [HttpGet("{identifier:id}")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Get(Identifier identifier, [FromQuery] Include? fields) { T? ret = await identifier.Match( id => Repository.GetOrDefault(id, fields), slug => Repository.GetOrDefault(slug, fields) ); if (ret == null) return NotFound(); return ret; } /// /// Get count /// /// /// Get the number of resources that match the filters. /// /// A list of filters to respect. /// How many resources matched that filter. /// Invalid filters. [HttpGet("count")] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task> GetCount([FromQuery] Filter filter) { return await Repository.GetCount(filter); } /// /// Get all /// /// /// Get all resources that match the given filter. /// /// Sort information about the query (sort by, sort order). /// Filter the returned items. /// How many items per page should be returned, where should the page start... /// The aditional fields to include in the result. /// A list of resources that match every filters. /// Invalid filters or sort information. [HttpGet] [PartialPermission(Kind.Read)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task>> GetAll( [FromQuery] Sort sortBy, [FromQuery] Filter? filter, [FromQuery] Pagination pagination, [FromQuery] Include? fields ) { ICollection resources = await Repository.GetAll(filter, sortBy, fields, pagination); return Page(resources, pagination.Limit); } /// /// Create new /// /// /// Create a new item and store it. You may leave the ID unspecified, it will be filed by Kyoo. /// /// The resource to create. /// The created resource. /// The resource in the request body is invalid. /// This item already exists (maybe a duplicated slug). [HttpPost] [PartialPermission(Kind.Create)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(ActionResult<>))] public virtual async Task> Create([FromBody] T resource) { return await Repository.Create(resource); } /// /// Edit /// /// /// Edit 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. /// /// The resource to edit. /// The edited resource. /// The resource in the request body is invalid. /// No item found with the specified ID (or slug). [HttpPut] [PartialPermission(Kind.Write)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Edit([FromBody] T resource) { if (resource.Id != Guid.Empty) return await Repository.Edit(resource); T old = await Repository.Get(resource.Slug); resource.Id = old.Id; return await Repository.Edit(resource); } /// /// Patch /// /// /// 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. /// /// The resource to patch. /// The edited resource. /// The resource in the request body is invalid. /// No item found with the specified ID (or slug). [HttpPatch] [PartialPermission(Kind.Write)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Patch([FromBody] Patch patch) { 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(patch.Slug); return await Repository.Patch(old.Id, patch.Apply); } /// /// Patch /// /// /// 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. /// /// The id or slug of the resource. /// The resource to patch. /// The edited resource. /// The resource in the request body is invalid. /// No item found with the specified ID (or slug). [HttpPatch("{identifier:id}")] [PartialPermission(Kind.Write)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Patch(Identifier identifier, [FromBody] Patch patch) { Guid id = await identifier.Match( id => Task.FromResult(id), async slug => (await Repository.Get(slug)).Id ); if (patch.Id.HasValue && patch.Id.Value != id) throw new ArgumentException("Can not edit id of a resource."); return await Repository.Patch(id, patch.Apply); } /// /// Delete an item /// /// /// Delete one item via it's ID or it's slug. /// /// The ID or slug of the resource to delete. /// The item has successfully been deleted. /// No item could be found with the given id or slug. [HttpDelete("{identifier:id}")] [PartialPermission(Kind.Delete)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(Identifier identifier) { await identifier.Match(id => Repository.Delete(id), slug => Repository.Delete(slug)); return NoContent(); } /// /// Delete all where /// /// /// Delete all items matching the given filters. If no filter is specified, delete all items. /// /// The list of filters. /// The item(s) has successfully been deleted. /// One or multiple filters are invalid. [HttpDelete] [PartialPermission(Kind.Delete)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))] public async Task Delete([FromQuery] Filter filter) { if (filter == null) return BadRequest( new RequestError("Incule a filter to delete items, all items won't be deleted.") ); await Repository.DeleteAll(filter); return NoContent(); } }