Skip to content

Commit

Permalink
Document version endpoints (#15946)
Browse files Browse the repository at this point in the history
* Rename/Move/duplicate PaginationService to facilitate conversion closer to the data layer

Duplication is because of internal modifier as we don't want to expose these temporary classes

* Move Guid to Int Extensions into core + add unittests

* Added Document version endpoints

Updated used services to use async methods

* Moved PaginationConverter into core so it can be used by the service layer

* Endpoint structure improvements

* Updating OpenApi.json

* Add greedy constructors for contentService tests

* Namespace changes and naming cleanup

* Update openapispec again...

* Refactor injected services

* PR suggestion updates

- Move endpoints into their own structural section as they are also in a different swagger section
- Naming improvements
- Allign PresentationFactories with similar classes
- Cleanup unused assignments
- Cleanup refactoring comments
- Improve obsoletion remarks

* Cleanup

* ResponseModel improvements

* OpenApi spec update

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
Co-authored-by: Elitsa <elm@umbraco.dk>
  • Loading branch information
3 people authored Apr 2, 2024
1 parent 1866b61 commit 95849c2
Show file tree
Hide file tree
Showing 21 changed files with 1,136 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;

[ApiVersion("1.0")]
public class AllDocumentVersionController : DocumentVersionControllerBase
{
private readonly IContentVersionService _contentVersionService;
private readonly IDocumentVersionPresentationFactory _documentVersionPresentationFactory;

public AllDocumentVersionController(
IContentVersionService contentVersionService,
IDocumentVersionPresentationFactory documentVersionPresentationFactory)
{
_contentVersionService = contentVersionService;
_documentVersionPresentationFactory = documentVersionPresentationFactory;
}

[MapToApiVersion("1.0")]
[HttpGet]
[ProducesResponseType(typeof(PagedViewModel<DocumentVersionItemResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> All([Required] Guid documentId, string? culture, int skip = 0, int take = 100)
{
Attempt<PagedModel<ContentVersionMeta>?, ContentVersionOperationStatus> attempt =
await _contentVersionService.GetPagedContentVersionsAsync(documentId, culture, skip, take);

var pagedViewModel = new PagedViewModel<DocumentVersionItemResponseModel>
{
Total = attempt.Result!.Total,
Items = await _documentVersionPresentationFactory.CreateMultipleAsync(attempt.Result!.Items),
};

return attempt.Success
? Ok(pagedViewModel)
: MapFailure(attempt.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;

[ApiVersion("1.0")]
public class ByKeyDocumentVersionController : DocumentVersionControllerBase
{
private readonly IContentVersionService _contentVersionService;
private readonly IUmbracoMapper _umbracoMapper;

public ByKeyDocumentVersionController(
IContentVersionService contentVersionService,
IUmbracoMapper umbracoMapper)
{
_contentVersionService = contentVersionService;
_umbracoMapper = umbracoMapper;
}

[MapToApiVersion("1.0")]
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(DocumentVersionResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ByKey(Guid id)
{
Attempt<IContent?, ContentVersionOperationStatus> attempt =
await _contentVersionService.GetAsync(id);

return attempt.Success
? Ok(_umbracoMapper.Map<DocumentVersionResponseModel>(attempt.Result))
: MapFailure(attempt.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;

[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}-version")]
[ApiExplorerSettings(GroupName = $"{nameof(Constants.UdiEntityType.Document)} Version")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public abstract class DocumentVersionControllerBase : ManagementApiControllerBase
{
protected IActionResult MapFailure(ContentVersionOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
{
ContentVersionOperationStatus.NotFound => NotFound(problemDetailsBuilder
.WithTitle("The requested version could not be found")
.Build()),
ContentVersionOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
.WithTitle("The requested document could not be found")
.Build()),
ContentVersionOperationStatus.InvalidSkipTake => SkipTakeToPagingProblem(),
ContentVersionOperationStatus.RollBackFailed => BadRequest(problemDetailsBuilder
.WithTitle("Rollback failed")
.WithDetail("An unspecified error occurred while rolling back the requested version. Please check the logs for additional information.")),
ContentVersionOperationStatus.RollBackCanceled => BadRequest(problemDetailsBuilder
.WithTitle("Request cancelled by notification")
.WithDetail("The request to roll back was cancelled by a notification handler.")
.Build()),
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
.WithTitle("Unknown content version operation status.")
.Build()),
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;

[ApiVersion("1.0")]
public class RollbackDocumentVersionController : DocumentVersionControllerBase
{
private readonly IContentVersionService _contentVersionService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public RollbackDocumentVersionController(
IContentVersionService contentVersionService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentVersionService = contentVersionService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[MapToApiVersion("1.0")]
[HttpPost("{id:guid}/rollback")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Rollback(Guid id, string? culture)
{
Attempt<ContentVersionOperationStatus> attempt =
await _contentVersionService.RollBackAsync(id, culture, CurrentUserKey(_backOfficeSecurityAccessor));

return attempt.Success
? Ok()
: MapFailure(attempt.Result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;

[ApiVersion("1.0")]
public class UpdatePreventCleanupDocumentVersionController : DocumentVersionControllerBase
{
private readonly IContentVersionService _contentVersionService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public UpdatePreventCleanupDocumentVersionController(
IContentVersionService contentVersionService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentVersionService = contentVersionService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[MapToApiVersion("1.0")]
[HttpPut("{id:guid}/prevent-cleanup")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Set(Guid id, bool preventCleanup)
{
Attempt<ContentVersionOperationStatus> attempt =
await _contentVersionService.SetPreventCleanupAsync(id, preventCleanup, CurrentUserKey(_backOfficeSecurityAccessor));

return attempt.Success
? Ok()
: MapFailure(attempt.Result);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,13 @@ protected static IUser CurrentUser(IBackOfficeSecurityAccessor backOfficeSecurit
protected static IActionResult OperationStatusResult<TEnum>(TEnum status, Func<ProblemDetailsBuilder, IActionResult> result)
where TEnum : Enum
=> result(new ProblemDetailsBuilder().WithOperationStatus(status));

protected BadRequestObjectResult SkipTakeToPagingProblem() =>
BadRequest(new ProblemDetails
{
Title = "Invalid skip/take",
Detail = "Skip must be a multiple of take - i.e. skip = 10, take = 5",
Status = StatusCodes.Status400BadRequest,
Type = "Error",
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
builder.Services.AddTransient<IDocumentEditingPresentationFactory, DocumentEditingPresentationFactory>();
builder.Services.AddTransient<IPublicAccessPresentationFactory, PublicAccessPresentationFactory>();
builder.Services.AddTransient<IDomainPresentationFactory, DomainPresentationFactory>();
builder.Services.AddTransient<IDocumentVersionPresentationFactory, DocumentVersionPresentationFactory>();

builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
.Add<DocumentMapDefinition>()
.Add<DomainMapDefinition>();
.Add<DomainMapDefinition>()
.Add<DocumentVersionMapDefinition>();

return builder;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Factories;

internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPresentationFactory
{
private readonly IEntityService _entityService;
private readonly IUserIdKeyResolver _userIdKeyResolver;

public DocumentVersionPresentationFactory(
IEntityService entityService,
IUserIdKeyResolver userIdKeyResolver)
{
_entityService = entityService;
_userIdKeyResolver = userIdKeyResolver;
}

public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion) =>
new(
contentVersion.VersionId.ToGuid(), // this is a magic guid since versions do not have keys in the DB
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentId, UmbracoObjectTypes.Document).Result),
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
.Result),
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
contentVersion.CurrentPublishedVersion,
contentVersion.CurrentDraftVersion,
contentVersion.PreventCleanup);

public async Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(IEnumerable<ContentVersionMeta> contentVersions) =>
await Task.WhenAll(contentVersions.Select(CreateAsync));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;

namespace Umbraco.Cms.Api.Management.Factories;

public interface IDocumentVersionPresentationFactory
{
Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion);

Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(
IEnumerable<ContentVersionMeta> contentVersions);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Umbraco.Cms.Api.Management.Mapping.Content;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Mapping.Document;

public class DocumentVersionMapDefinition : ContentMapDefinition<IContent, DocumentValueModel, DocumentVariantResponseModel>, IMapDefinition
{
public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection)
: base(propertyEditorCollection)
{
}

public void DefineMaps(IUmbracoMapper mapper)
{
mapper.Define<IContent, DocumentVersionResponseModel>((_, _) => new DocumentVersionResponseModel(), Map);
}

private void Map(IContent source, DocumentVersionResponseModel target, MapperContext context)
{
target.Id = source.VersionId.ToGuid(); // this is a magic guid since versions do not have Guids in the DB
target.Document = new ReferenceByIdModel(source.Key);
target.DocumentType = context.Map<DocumentTypeReferenceResponseModel>(source.ContentType)!;
target.Values = MapValueViewModels(source.Properties);
target.Variants = MapVariantViewModels(
source,
(culture, _, documentVariantViewModel) =>
{
documentVariantViewModel.State = DocumentVariantStateHelper.GetState(source, culture);
documentVariantViewModel.PublishDate = culture == null
? source.PublishDate
: source.GetPublishDate(culture);
});
}
}
Loading

0 comments on commit 95849c2

Please sign in to comment.