// 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.Linq.Expressions; 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] [ResourceView] public class CrudApi : BaseApi where T : class, IResource { /// /// 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 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) { T? ret = await identifier.Match( id => Repository.GetOrDefault(id), slug => Repository.GetOrDefault(slug) ); 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] Dictionary where) { return await Repository.GetCount(ApiHelper.ParseWhere(where)); } /// /// 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... /// 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] Dictionary where, [FromQuery] Pagination pagination) { ICollection resources = await Repository.GetAll( ApiHelper.ParseWhere(where), sortBy, 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 > 0) 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] PartialResource resource) { if (resource.Id.HasValue) return await Repository.Patch(resource.Id.Value, TryUpdateModelAsync); if (resource.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); } /// /// 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] Dictionary where) { Expression>? w = ApiHelper.ParseWhere(where); if (w == null) return BadRequest(new RequestError("Incule a filter to delete items, all items won't be deleted.")); await Repository.DeleteAll(w); return NoContent(); } } }