Skip to content

Commit

Permalink
V14: Blueprint CRUD endpoints (#15947)
Browse files Browse the repository at this point in the history
* Get blueprint by key

* Renaming

* Implementing DeleteBlueprintAsync

* Fixing tests to use the new method

* Making ContentControllerBase abstract

* Cleanup

* Implementing Delete blueprint endpoint

* Revert obsoletion in ContentService.cs

* More reverting

* Remove usings

* Introducing IContentBlueprintEditingService

* Refactor Get and Delete blueprint endpoints to use the new IContentBlueprintEditingService

* Fix base inheritance case in SchemaIdSelector

* Creating RequestModelBase for UpdateDocument to be reused in both document and blueprint models

* Creating DocumentResponseModelBase to be reused in both document and blueprint models

* Renamed blueprint response model for item endpoint to be aligned with the rest of the item models

* More renaming changes of the DocumentBlueprintItemResponseModel

* Refactor ByKeyDocumentBlueprintController to make use of the new blueprint models

* New blueprint models and mapping

* Adding UpdateAsync to ContentBlueprintEditingService

* Adding IDocumentBlueprintEditingPresentationFactory.cs

* Adding UpdateDocumentBlueprintController.cs

* Adding methods required from the base

* Fixing bug in document type mapping - mapping incorrect key

* Cleanup

* Fix item endpoint

* Adding MapCreateModel

* Adding create model

* Creating request model base + related classes

* Another request model

* Blueprint editing service

* Adding create controllers

* Adding DuplicateName operation status for blueprints and handling it

* Updating OpenApi.json

* Fix comment

* Fix mapping

* Adding comments

* Passing in id for create blueprint from document model

* Mapping default state to Draft - no need to calculate it, it will always be that for blueprints

* Cleanup

* Update OpenApi.json

* Review comments

* Fix policies

* More policy updates
  • Loading branch information
elit0451 authored Apr 2, 2024
1 parent 95849c2 commit 37734ef
Show file tree
Hide file tree
Showing 37 changed files with 1,305 additions and 75 deletions.
3 changes: 2 additions & 1 deletion src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdSelector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ private string HandleGenerics(string name, Type type)
})
.Where(info => info.GenericTypes != null);

// We need FirstOrDefault() because through inheritance, the attribute can be on several classes implementing a base
var matchingType = assignableTypesWithAttributeInfo
.SingleOrDefault(t => t.GenericTypes!.Length == type.GenericTypeArguments.Length
.FirstOrDefault(t => t.GenericTypes!.Length == type.GenericTypeArguments.Length
&& t.GenericTypes.Intersect(type.GenericTypeArguments).Count() ==
type.GenericTypeArguments.Length && t.SchemaName.IsNullOrWhiteSpace() == false);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

public class ContentControllerBase : ManagementApiControllerBase
public abstract class ContentControllerBase : ManagementApiControllerBase
{
protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status)
=> OperationStatusResult(status, problemDetailsBuilder => status switch
Expand Down Expand Up @@ -68,6 +68,10 @@ protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperat
.WithTitle("Invalid Id")
.WithDetail("The supplied id is already in use.")
.Build()),
ContentEditingOperationStatus.DuplicateName => BadRequest(problemDetailsBuilder
.WithTitle("Duplicate name")
.WithDetail("The supplied name is already in use for the same content type.")
.Build()),
ContentEditingOperationStatus.Unknown => StatusCode(
StatusCodes.Status500InternalServerError,
problemDetailsBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,5 @@ public async Task<IActionResult> Create(CreateDataTypeRequestModel createDataTyp
return result.Success
? CreatedAtId<ByKeyDataTypeController>(controller => nameof(controller.ByKey), result.Result.Key)
: DataTypeOperationStatusResult(result.Status);





}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.Authorization;

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

[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)]
public class ByKeyDocumentBlueprintController : DocumentBlueprintControllerBase
{
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IUmbracoMapper _umbracoMapper;

public ByKeyDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IUmbracoMapper umbracoMapper)
{
_contentBlueprintEditingService = contentBlueprintEditingService;
_umbracoMapper = umbracoMapper;
}

[HttpGet("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(DocumentBlueprintResponseModel), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> ByKey(Guid id)
{
IContent? blueprint = await _contentBlueprintEditingService.GetAsync(id);
if (blueprint == null)
{
return DocumentBlueprintNotFound();
}

return Ok(_umbracoMapper.Map<DocumentBlueprintResponseModel>(blueprint));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;

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

[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class CreateDocumentBlueprintController : DocumentBlueprintControllerBase
{
private readonly IDocumentBlueprintEditingPresentationFactory _blueprintEditingPresentationFactory;
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public CreateDocumentBlueprintController(
IDocumentBlueprintEditingPresentationFactory blueprintEditingPresentationFactory,
IContentBlueprintEditingService contentBlueprintEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_blueprintEditingPresentationFactory = blueprintEditingPresentationFactory;
_contentBlueprintEditingService = contentBlueprintEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPost]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Create(CreateDocumentBlueprintRequestModel requestModel)
{
ContentBlueprintCreateModel model = _blueprintEditingPresentationFactory.MapCreateModel(requestModel);

// We don't need to validate user access because we "only" require access to the Settings section to create new blueprints from scratch
Attempt<ContentCreateResult, ContentEditingOperationStatus> result = await _contentBlueprintEditingService.CreateAsync(model, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? CreatedAtId<ByKeyDocumentBlueprintController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;

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

[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)]
public class CreateDocumentBlueprintFromDocumentController : DocumentBlueprintControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public CreateDocumentBlueprintFromDocumentController(
IAuthorizationService authorizationService,
IContentBlueprintEditingService contentBlueprintEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_contentBlueprintEditingService = contentBlueprintEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

/// <summary>
/// Creates a blueprint from a content item.
/// </summary>
[HttpPost("from-document")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreateFromDocument(CreateDocumentBlueprintFromDocumentRequestModel fromDocumentRequestModel)
{
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionCreateBlueprintFromContent.ActionLetter, fromDocumentRequestModel.Document.Id),
AuthorizationPolicies.ContentPermissionByResource);

if (!authorizationResult.Succeeded)
{
return Forbidden();
}

Attempt<ContentCreateResult, ContentEditingOperationStatus> result =
await _contentBlueprintEditingService.CreateFromContentAsync(
fromDocumentRequestModel.Document.Id,
fromDocumentRequestModel.Name,
fromDocumentRequestModel.Id,
CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? CreatedAtId<ByKeyDocumentBlueprintController>(controller => nameof(controller.ByKey), result.Result.Content!.Key)
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;

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

[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class DeleteDocumentBlueprintController : DocumentBlueprintControllerBase
{
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public DeleteDocumentBlueprintController(IContentBlueprintEditingService contentBlueprintEditingService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_contentBlueprintEditingService = contentBlueprintEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpDelete("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Delete(Guid id)
{
Attempt<IContent?, ContentEditingOperationStatus> result = await _contentBlueprintEditingService.DeleteAsync(id, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Content;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Services.OperationStatus;

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

[VersionedApiBackOfficeRoute(Constants.UdiEntityType.DocumentBlueprint)]
[ApiExplorerSettings(GroupName = "Document Blueprint")]
public abstract class DocumentBlueprintControllerBase : ContentControllerBase
{
protected IActionResult DocumentBlueprintNotFound()
=> OperationStatusResult(ContentEditingOperationStatus.NotFound, problemDetailsBuilder
=> NotFound(problemDetailsBuilder
.WithTitle("The document blueprint could not be found")
.Build()));
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ public ItemDocumentBlueprintController(IEntityService entityService, IDocumentPr

[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintResponseModel>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(IEnumerable<DocumentBlueprintItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
{
IEnumerable<IDocumentEntitySlim> documents = _entityService.GetAll(UmbracoObjectTypes.Document, ids.ToArray()).Select(x => x as IDocumentEntitySlim).WhereNotNull();
IEnumerable<DocumentBlueprintResponseModel> responseModels = documents.Select(x => _documentPresentationFactory.CreateBlueprintItemResponseModel(x));
IEnumerable<IDocumentEntitySlim> documents = _entityService
.GetAll(UmbracoObjectTypes.DocumentBlueprint, ids.ToArray())
.Select(x => x as IDocumentEntitySlim)
.WhereNotNull();
IEnumerable<DocumentBlueprintItemResponseModel> responseModels = documents.Select(x => _documentPresentationFactory.CreateBlueprintItemResponseModel(x));
return await Task.FromResult(Ok(responseModels));
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Api.Management.Controllers.DocumentBlueprint.Tree;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;

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

[ApiVersion("1.0")]
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
public class UpdateDocumentBlueprintController : DocumentBlueprintControllerBase
{
private readonly IDocumentBlueprintEditingPresentationFactory _blueprintEditingPresentationFactory;
private readonly IContentBlueprintEditingService _contentBlueprintEditingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public UpdateDocumentBlueprintController(
IDocumentBlueprintEditingPresentationFactory blueprintEditingPresentationFactory,
IContentBlueprintEditingService contentBlueprintEditingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_blueprintEditingPresentationFactory = blueprintEditingPresentationFactory;
_contentBlueprintEditingService = contentBlueprintEditingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPut("{id:guid}")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, UpdateDocumentBlueprintRequestModel requestModel)
{
ContentBlueprintUpdateModel model = _blueprintEditingPresentationFactory.MapUpdateModel(requestModel);

// We don't need to validate user access because we "only" require access to the Settings section to update blueprints
Attempt<ContentUpdateResult, ContentEditingOperationStatus> result = await _contentBlueprintEditingService.UpdateAsync(id, model, CurrentUserKey(_backOfficeSecurityAccessor));

return result.Success
? Ok()
: ContentEditingOperationStatusResult(result.Status);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static IUmbracoBuilder AddDocuments(this IUmbracoBuilder builder)
builder.Services.AddTransient<IDocumentNotificationPresentationFactory, DocumentNotificationPresentationFactory>();
builder.Services.AddTransient<IDocumentUrlFactory, DocumentUrlFactory>();
builder.Services.AddTransient<IDocumentEditingPresentationFactory, DocumentEditingPresentationFactory>();
builder.Services.AddTransient<IDocumentBlueprintEditingPresentationFactory, DocumentBlueprintEditingPresentationFactory>();
builder.Services.AddTransient<IPublicAccessPresentationFactory, PublicAccessPresentationFactory>();
builder.Services.AddTransient<IDomainPresentationFactory, DomainPresentationFactory>();
builder.Services.AddTransient<IDocumentVersionPresentationFactory, DocumentVersionPresentationFactory>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint;
using Umbraco.Cms.Core.Models.ContentEditing;

namespace Umbraco.Cms.Api.Management.Factories;

internal sealed class DocumentBlueprintEditingPresentationFactory : ContentEditingPresentationFactory<DocumentValueModel, DocumentVariantRequestModel>, IDocumentBlueprintEditingPresentationFactory
{
public ContentBlueprintCreateModel MapCreateModel(CreateDocumentBlueprintRequestModel requestModel)
{
ContentBlueprintCreateModel model = MapContentEditingModel<ContentBlueprintCreateModel>(requestModel);
model.Key = requestModel.Id;
model.ContentTypeKey = requestModel.DocumentType.Id;
model.ParentKey = requestModel.Parent?.Id;

return model;
}

public ContentBlueprintUpdateModel MapUpdateModel(UpdateDocumentBlueprintRequestModel requestModel)
{
ContentBlueprintUpdateModel model = MapContentEditingModel<ContentBlueprintUpdateModel>(requestModel);
return model;
}
}
Loading

0 comments on commit 37734ef

Please sign in to comment.