Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document version endpoints #15946

Merged
merged 15 commits into from Apr 2, 2024
@@ -0,0 +1,46 @@
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;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;

namespace Umbraco.Cms.Api.Management.Controllers.Document.Versions;
Migaroez marked this conversation as resolved.
Show resolved Hide resolved

[ApiController]
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}-version")]
[ApiExplorerSettings(GroupName = $"{Constants.UdiEntityType.Document} Version")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public class DocumentVersionControllerBase : ManagementApiControllerBase
Migaroez marked this conversation as resolved.
Show resolved Hide resolved
{
protected readonly IContentVersionService ContentVersionService;
elit0451 marked this conversation as resolved.
Show resolved Hide resolved

public DocumentVersionControllerBase(IContentVersionService contentVersionService)
{
ContentVersionService = contentVersionService;
}

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()),
});
}
@@ -0,0 +1,40 @@
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.Document.Versions;

[ApiVersion("1.0")]
public class GetByKeyDocumentVersionController : DocumentVersionControllerBase
Migaroez marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly IUmbracoMapper _umbracoMapper;

public GetByKeyDocumentVersionController(
IContentVersionService contentVersionService,
IUmbracoMapper umbracoMapper)
: base(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> Get(Guid id)
{
Attempt<IContent?, ContentVersionOperationStatus> attempt =
await ContentVersionService.GetAsync(id);

return attempt.Success is true
? Ok(_umbracoMapper.Map<DocumentVersionResponseModel>(attempt.Result))
: MapFailure(attempt.Status);
}
}
@@ -0,0 +1,44 @@
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.Document.Versions;

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

public GetDocumentVersionsController(
IContentVersionService contentVersionService,
IDocumentVersionPresentationFactory documentVersionPresentationFactory)
: base(contentVersionService)
{
_documentVersionPresentationFactory = documentVersionPresentationFactory;
}

// move to item?
[MapToApiVersion("1.0")]
[HttpGet]
[ProducesResponseType(typeof(PagedViewModel<DocumentVersionItemResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Get(Guid documentId, string? culture, int skip = 0, int take = 100)
{
// old ContentController.GetRollbackVersions()
// get all versions for a given document
Attempt<PagedModel<ContentVersionMeta>?, ContentVersionOperationStatus> attempt =
await ContentVersionService.GetContentVersionsAsync(documentId, culture, skip, take);

return attempt.Success is true
? Ok(await _documentVersionPresentationFactory.CreatedPagedResponseModelAsync(attempt.Result!))
: MapFailure(attempt.Status);
}
}
@@ -0,0 +1,38 @@
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.Document.Versions;

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

public RollbackDocumentVersionController(
IContentVersionService contentVersionService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
: base(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 is true
? Ok()
: MapFailure(attempt.Result);
}
}
@@ -0,0 +1,38 @@
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.Document.Versions;

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

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

[MapToApiVersion("1.0")]
[HttpPut("{id:guid}/preventCleanup")]
[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 is true
? Ok()
: MapFailure(attempt.Result);
}
}
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",
});
}
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
@@ -0,0 +1,42 @@
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
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> CreateResponseModelAsync(ContentVersionMeta contentVersion) =>
new(
new ReferenceByIdModel(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<PagedViewModel<DocumentVersionItemResponseModel>> CreatedPagedResponseModelAsync(
PagedModel<ContentVersionMeta> pagedContentVersionMeta) =>
new()
{
Total = pagedContentVersionMeta.Total,
Items = await Task.WhenAll(pagedContentVersionMeta.Items.Select(CreateResponseModelAsync)),
};
}
@@ -0,0 +1,13 @@
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core.Models;

namespace Umbraco.Cms.Api.Management.Factories;

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

Task<PagedViewModel<DocumentVersionItemResponseModel>> CreatedPagedResponseModelAsync(
Migaroez marked this conversation as resolved.
Show resolved Hide resolved
PagedModel<ContentVersionMeta> pagedContentVersionMeta);
}
@@ -0,0 +1,45 @@
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.Document.Collection;
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Mapping;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Extensions;

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

public class DocumentVersionMapDefinition : ContentMapDefinition<IContent, DocumentValueModel, DocumentVariantResponseModel>, IMapDefinition
{
private readonly CommonMapper _commonMapper;
Migaroez marked this conversation as resolved.
Show resolved Hide resolved

public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection, CommonMapper commonMapper)
: base(propertyEditorCollection)
{
_commonMapper = commonMapper;
}

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);
});
}
}