Skip to content

Commit 95849c2

Browse files
Migaroezelit0451
andauthored
Document version endpoints (#15946)
* 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>
1 parent 1866b61 commit 95849c2

21 files changed

Lines changed: 1136 additions & 15 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Asp.Versioning;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
6+
using Umbraco.Cms.Api.Management.Factories;
7+
using Umbraco.Cms.Api.Management.ViewModels.Document;
8+
using Umbraco.Cms.Core;
9+
using Umbraco.Cms.Core.Models;
10+
using Umbraco.Cms.Core.Services;
11+
using Umbraco.Cms.Core.Services.OperationStatus;
12+
13+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;
14+
15+
[ApiVersion("1.0")]
16+
public class AllDocumentVersionController : DocumentVersionControllerBase
17+
{
18+
private readonly IContentVersionService _contentVersionService;
19+
private readonly IDocumentVersionPresentationFactory _documentVersionPresentationFactory;
20+
21+
public AllDocumentVersionController(
22+
IContentVersionService contentVersionService,
23+
IDocumentVersionPresentationFactory documentVersionPresentationFactory)
24+
{
25+
_contentVersionService = contentVersionService;
26+
_documentVersionPresentationFactory = documentVersionPresentationFactory;
27+
}
28+
29+
[MapToApiVersion("1.0")]
30+
[HttpGet]
31+
[ProducesResponseType(typeof(PagedViewModel<DocumentVersionItemResponseModel>), StatusCodes.Status200OK)]
32+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
33+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
34+
public async Task<IActionResult> All([Required] Guid documentId, string? culture, int skip = 0, int take = 100)
35+
{
36+
Attempt<PagedModel<ContentVersionMeta>?, ContentVersionOperationStatus> attempt =
37+
await _contentVersionService.GetPagedContentVersionsAsync(documentId, culture, skip, take);
38+
39+
var pagedViewModel = new PagedViewModel<DocumentVersionItemResponseModel>
40+
{
41+
Total = attempt.Result!.Total,
42+
Items = await _documentVersionPresentationFactory.CreateMultipleAsync(attempt.Result!.Items),
43+
};
44+
45+
return attempt.Success
46+
? Ok(pagedViewModel)
47+
: MapFailure(attempt.Status);
48+
}
49+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Umbraco.Cms.Api.Management.ViewModels.Document;
5+
using Umbraco.Cms.Core;
6+
using Umbraco.Cms.Core.Mapping;
7+
using Umbraco.Cms.Core.Models;
8+
using Umbraco.Cms.Core.Services;
9+
using Umbraco.Cms.Core.Services.OperationStatus;
10+
11+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;
12+
13+
[ApiVersion("1.0")]
14+
public class ByKeyDocumentVersionController : DocumentVersionControllerBase
15+
{
16+
private readonly IContentVersionService _contentVersionService;
17+
private readonly IUmbracoMapper _umbracoMapper;
18+
19+
public ByKeyDocumentVersionController(
20+
IContentVersionService contentVersionService,
21+
IUmbracoMapper umbracoMapper)
22+
{
23+
_contentVersionService = contentVersionService;
24+
_umbracoMapper = umbracoMapper;
25+
}
26+
27+
[MapToApiVersion("1.0")]
28+
[HttpGet("{id:guid}")]
29+
[ProducesResponseType(typeof(DocumentVersionResponseModel), StatusCodes.Status200OK)]
30+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
31+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
32+
public async Task<IActionResult> ByKey(Guid id)
33+
{
34+
Attempt<IContent?, ContentVersionOperationStatus> attempt =
35+
await _contentVersionService.GetAsync(id);
36+
37+
return attempt.Success
38+
? Ok(_umbracoMapper.Map<DocumentVersionResponseModel>(attempt.Result))
39+
: MapFailure(attempt.Status);
40+
}
41+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Umbraco.Cms.Api.Management.Routing;
5+
using Umbraco.Cms.Core;
6+
using Umbraco.Cms.Core.Services.OperationStatus;
7+
using Umbraco.Cms.Web.Common.Authorization;
8+
9+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;
10+
11+
[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}-version")]
12+
[ApiExplorerSettings(GroupName = $"{nameof(Constants.UdiEntityType.Document)} Version")]
13+
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
14+
public abstract class DocumentVersionControllerBase : ManagementApiControllerBase
15+
{
16+
protected IActionResult MapFailure(ContentVersionOperationStatus status)
17+
=> OperationStatusResult(status, problemDetailsBuilder => status switch
18+
{
19+
ContentVersionOperationStatus.NotFound => NotFound(problemDetailsBuilder
20+
.WithTitle("The requested version could not be found")
21+
.Build()),
22+
ContentVersionOperationStatus.ContentNotFound => NotFound(problemDetailsBuilder
23+
.WithTitle("The requested document could not be found")
24+
.Build()),
25+
ContentVersionOperationStatus.InvalidSkipTake => SkipTakeToPagingProblem(),
26+
ContentVersionOperationStatus.RollBackFailed => BadRequest(problemDetailsBuilder
27+
.WithTitle("Rollback failed")
28+
.WithDetail("An unspecified error occurred while rolling back the requested version. Please check the logs for additional information.")),
29+
ContentVersionOperationStatus.RollBackCanceled => BadRequest(problemDetailsBuilder
30+
.WithTitle("Request cancelled by notification")
31+
.WithDetail("The request to roll back was cancelled by a notification handler.")
32+
.Build()),
33+
_ => StatusCode(StatusCodes.Status500InternalServerError, problemDetailsBuilder
34+
.WithTitle("Unknown content version operation status.")
35+
.Build()),
36+
});
37+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Umbraco.Cms.Core;
5+
using Umbraco.Cms.Core.Security;
6+
using Umbraco.Cms.Core.Services;
7+
using Umbraco.Cms.Core.Services.OperationStatus;
8+
9+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;
10+
11+
[ApiVersion("1.0")]
12+
public class RollbackDocumentVersionController : DocumentVersionControllerBase
13+
{
14+
private readonly IContentVersionService _contentVersionService;
15+
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
16+
17+
public RollbackDocumentVersionController(
18+
IContentVersionService contentVersionService,
19+
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
20+
{
21+
_contentVersionService = contentVersionService;
22+
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
23+
}
24+
25+
[MapToApiVersion("1.0")]
26+
[HttpPost("{id:guid}/rollback")]
27+
[ProducesResponseType(StatusCodes.Status200OK)]
28+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
29+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
30+
public async Task<IActionResult> Rollback(Guid id, string? culture)
31+
{
32+
Attempt<ContentVersionOperationStatus> attempt =
33+
await _contentVersionService.RollBackAsync(id, culture, CurrentUserKey(_backOfficeSecurityAccessor));
34+
35+
return attempt.Success
36+
? Ok()
37+
: MapFailure(attempt.Result);
38+
}
39+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Umbraco.Cms.Core;
5+
using Umbraco.Cms.Core.Security;
6+
using Umbraco.Cms.Core.Services;
7+
using Umbraco.Cms.Core.Services.OperationStatus;
8+
9+
namespace Umbraco.Cms.Api.Management.Controllers.DocumentVersion;
10+
11+
[ApiVersion("1.0")]
12+
public class UpdatePreventCleanupDocumentVersionController : DocumentVersionControllerBase
13+
{
14+
private readonly IContentVersionService _contentVersionService;
15+
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
16+
17+
public UpdatePreventCleanupDocumentVersionController(
18+
IContentVersionService contentVersionService,
19+
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
20+
{
21+
_contentVersionService = contentVersionService;
22+
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
23+
}
24+
25+
[MapToApiVersion("1.0")]
26+
[HttpPut("{id:guid}/prevent-cleanup")]
27+
[ProducesResponseType(StatusCodes.Status200OK)]
28+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
29+
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
30+
public async Task<IActionResult> Set(Guid id, bool preventCleanup)
31+
{
32+
Attempt<ContentVersionOperationStatus> attempt =
33+
await _contentVersionService.SetPreventCleanupAsync(id, preventCleanup, CurrentUserKey(_backOfficeSecurityAccessor));
34+
35+
return attempt.Success
36+
? Ok()
37+
: MapFailure(attempt.Result);
38+
}
39+
}

src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,13 @@ protected static IUser CurrentUser(IBackOfficeSecurityAccessor backOfficeSecurit
6262
protected static IActionResult OperationStatusResult<TEnum>(TEnum status, Func<ProblemDetailsBuilder, IActionResult> result)
6363
where TEnum : Enum
6464
=> result(new ProblemDetailsBuilder().WithOperationStatus(status));
65+
66+
protected BadRequestObjectResult SkipTakeToPagingProblem() =>
67+
BadRequest(new ProblemDetails
68+
{
69+
Title = "Invalid skip/take",
70+
Detail = "Skip must be a multiple of take - i.e. skip = 10, take = 5",
71+
Status = StatusCodes.Status400BadRequest,
72+
Type = "Error",
73+
});
6574
}

src/Umbraco.Cms.Api.Management/DependencyInjection/DocumentBuilderExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
1616
builder.Services.AddTransient<IDocumentEditingPresentationFactory, DocumentEditingPresentationFactory>();
1717
builder.Services.AddTransient<IPublicAccessPresentationFactory, PublicAccessPresentationFactory>();
1818
builder.Services.AddTransient<IDomainPresentationFactory, DomainPresentationFactory>();
19+
builder.Services.AddTransient<IDocumentVersionPresentationFactory, DocumentVersionPresentationFactory>();
1920

2021
builder.WithCollectionBuilder<MapDefinitionCollectionBuilder>()
2122
.Add<DocumentMapDefinition>()
22-
.Add<DomainMapDefinition>();
23+
.Add<DomainMapDefinition>()
24+
.Add<DocumentVersionMapDefinition>();
2325

2426
return builder;
2527
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Umbraco.Cms.Api.Management.ViewModels;
2+
using Umbraco.Cms.Api.Management.ViewModels.Document;
3+
using Umbraco.Cms.Core.Models;
4+
using Umbraco.Cms.Core.Services;
5+
using Umbraco.Extensions;
6+
7+
namespace Umbraco.Cms.Api.Management.Factories;
8+
9+
internal sealed class DocumentVersionPresentationFactory : IDocumentVersionPresentationFactory
10+
{
11+
private readonly IEntityService _entityService;
12+
private readonly IUserIdKeyResolver _userIdKeyResolver;
13+
14+
public DocumentVersionPresentationFactory(
15+
IEntityService entityService,
16+
IUserIdKeyResolver userIdKeyResolver)
17+
{
18+
_entityService = entityService;
19+
_userIdKeyResolver = userIdKeyResolver;
20+
}
21+
22+
public async Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion) =>
23+
new(
24+
contentVersion.VersionId.ToGuid(), // this is a magic guid since versions do not have keys in the DB
25+
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentId, UmbracoObjectTypes.Document).Result),
26+
new ReferenceByIdModel(_entityService.GetKey(contentVersion.ContentTypeId, UmbracoObjectTypes.DocumentType)
27+
.Result),
28+
new ReferenceByIdModel(await _userIdKeyResolver.GetAsync(contentVersion.UserId)),
29+
new DateTimeOffset(contentVersion.VersionDate, TimeSpan.Zero), // todo align with datetime offset rework
30+
contentVersion.CurrentPublishedVersion,
31+
contentVersion.CurrentDraftVersion,
32+
contentVersion.PreventCleanup);
33+
34+
public async Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(IEnumerable<ContentVersionMeta> contentVersions) =>
35+
await Task.WhenAll(contentVersions.Select(CreateAsync));
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Umbraco.Cms.Api.Management.ViewModels.Document;
2+
using Umbraco.Cms.Core.Models;
3+
4+
namespace Umbraco.Cms.Api.Management.Factories;
5+
6+
public interface IDocumentVersionPresentationFactory
7+
{
8+
Task<DocumentVersionItemResponseModel> CreateAsync(ContentVersionMeta contentVersion);
9+
10+
Task<IEnumerable<DocumentVersionItemResponseModel>> CreateMultipleAsync(
11+
IEnumerable<ContentVersionMeta> contentVersions);
12+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Umbraco.Cms.Api.Management.Mapping.Content;
2+
using Umbraco.Cms.Api.Management.ViewModels;
3+
using Umbraco.Cms.Api.Management.ViewModels.Document;
4+
using Umbraco.Cms.Api.Management.ViewModels.DocumentType;
5+
using Umbraco.Cms.Core.Mapping;
6+
using Umbraco.Cms.Core.Models;
7+
using Umbraco.Cms.Core.PropertyEditors;
8+
using Umbraco.Extensions;
9+
10+
namespace Umbraco.Cms.Api.Management.Mapping.Document;
11+
12+
public class DocumentVersionMapDefinition : ContentMapDefinition<IContent, DocumentValueModel, DocumentVariantResponseModel>, IMapDefinition
13+
{
14+
public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection)
15+
: base(propertyEditorCollection)
16+
{
17+
}
18+
19+
public void DefineMaps(IUmbracoMapper mapper)
20+
{
21+
mapper.Define<IContent, DocumentVersionResponseModel>((_, _) => new DocumentVersionResponseModel(), Map);
22+
}
23+
24+
private void Map(IContent source, DocumentVersionResponseModel target, MapperContext context)
25+
{
26+
target.Id = source.VersionId.ToGuid(); // this is a magic guid since versions do not have Guids in the DB
27+
target.Document = new ReferenceByIdModel(source.Key);
28+
target.DocumentType = context.Map<DocumentTypeReferenceResponseModel>(source.ContentType)!;
29+
target.Values = MapValueViewModels(source.Properties);
30+
target.Variants = MapVariantViewModels(
31+
source,
32+
(culture, _, documentVariantViewModel) =>
33+
{
34+
documentVariantViewModel.State = DocumentVariantStateHelper.GetState(source, culture);
35+
documentVariantViewModel.PublishDate = culture == null
36+
? source.PublishDate
37+
: source.GetPublishDate(culture);
38+
});
39+
}
40+
}

0 commit comments

Comments
 (0)