Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Temporary CRUD API for content types (#14334)
* Add temporary controllers for document type CRUD * Update OpenAPI json * Review comments * Add comment for future refactoring
- Loading branch information
Showing
24 changed files
with
884 additions
and
56 deletions.
There are no files selected for viewing
43 changes: 43 additions & 0 deletions
43
src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateDocumentTypeController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using Asp.Versioning; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType; | ||
using Umbraco.Cms.Core; | ||
using Umbraco.Cms.Core.Models; | ||
using Umbraco.Cms.Core.Services; | ||
using Umbraco.Cms.Core.Services.OperationStatus; | ||
using Umbraco.Cms.Core.Strings; | ||
|
||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; | ||
|
||
[ApiVersion("1.0")] | ||
public class CreateDocumentTypeController : CreateUpdateDocumentTypeControllerBase | ||
{ | ||
private readonly IShortStringHelper _shortStringHelper; | ||
|
||
public CreateDocumentTypeController(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) | ||
: base(contentTypeService, dataTypeService, shortStringHelper, templateService) | ||
=> _shortStringHelper = shortStringHelper; | ||
|
||
[HttpPost] | ||
[MapToApiVersion("1.0")] | ||
[ProducesResponseType(StatusCodes.Status200OK)] | ||
[ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
public async Task<IActionResult> Create(CreateDocumentTypeRequestModel requestModel) | ||
{ | ||
// FIXME: support document type folders (and creation within folders) | ||
const int parentId = Constants.System.Root; | ||
|
||
if (requestModel.Compositions.Any()) | ||
{ | ||
return await Task.FromResult(BadRequest("Compositions and inheritance is not yet supported by this endpoint")); | ||
} | ||
|
||
IContentType contentType = new ContentType(_shortStringHelper, parentId); | ||
ContentTypeOperationStatus result = HandleRequest<CreateDocumentTypeRequestModel, CreateDocumentTypePropertyTypeRequestModel, CreateDocumentTypePropertyTypeContainerRequestModel>(contentType, requestModel); | ||
|
||
return result == ContentTypeOperationStatus.Success | ||
? CreatedAtAction<ByKeyDocumentTypeController>(controller => nameof(controller.ByKey), contentType.Key) | ||
: BadRequest(result); | ||
} | ||
} |
222 changes: 222 additions & 0 deletions
222
...aco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,222 @@ | ||
using Umbraco.Cms.Api.Management.ViewModels.ContentType; | ||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType; | ||
using Umbraco.Cms.Core.Models; | ||
using Umbraco.Cms.Core.Models.ContentEditing; | ||
using Umbraco.Cms.Core.Models.PublishedContent; | ||
using Umbraco.Cms.Core.Services; | ||
using Umbraco.Cms.Core.Services.OperationStatus; | ||
using Umbraco.Cms.Core.Strings; | ||
using Umbraco.Extensions; | ||
using ContentTypeSort = Umbraco.Cms.Core.Models.ContentTypeSort; | ||
|
||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; | ||
|
||
// FIXME: pretty much everything here should be moved to mappers and possibly new services for content type editing - like the ContentEditingService for content | ||
public abstract class CreateUpdateDocumentTypeControllerBase : DocumentTypeControllerBase | ||
{ | ||
private readonly IContentTypeService _contentTypeService; | ||
private readonly IDataTypeService _dataTypeService; | ||
private readonly IShortStringHelper _shortStringHelper; | ||
private readonly ITemplateService _templateService; | ||
|
||
protected CreateUpdateDocumentTypeControllerBase(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) | ||
{ | ||
_contentTypeService = contentTypeService; | ||
_dataTypeService = dataTypeService; | ||
_shortStringHelper = shortStringHelper; | ||
_templateService = templateService; | ||
} | ||
|
||
protected ContentTypeOperationStatus HandleRequest<TRequestModel, TPropertyType, TPropertyTypeContainer>(IContentType contentType, TRequestModel requestModel) | ||
where TRequestModel : ContentTypeModelBase<TPropertyType, TPropertyTypeContainer>, IDocumentTypeRequestModel | ||
where TPropertyType : PropertyTypeModelBase | ||
where TPropertyTypeContainer : PropertyTypeContainerModelBase | ||
{ | ||
// validate content type alias | ||
if (contentType.Alias.Equals(requestModel.Alias) is false) | ||
{ | ||
if (_contentTypeService.GetAllContentTypeAliases().Contains(requestModel.Alias)) | ||
{ | ||
return ContentTypeOperationStatus.DuplicateAlias; | ||
} | ||
|
||
var reservedModelAliases = new[] { "system" }; | ||
if (reservedModelAliases.InvariantContains(requestModel.Alias)) | ||
{ | ||
return ContentTypeOperationStatus.InvalidAlias; | ||
} | ||
} | ||
|
||
// validate properties | ||
var reservedPropertyTypeNames = typeof(IPublishedContent).GetProperties().Select(x => x.Name) | ||
.Union(typeof(IPublishedContent).GetMethods().Select(x => x.Name)) | ||
.ToArray(); | ||
foreach (TPropertyType propertyType in requestModel.Properties) | ||
{ | ||
if (propertyType.Alias.Equals(requestModel.Alias, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return ContentTypeOperationStatus.InvalidPropertyTypeAlias; | ||
} | ||
|
||
if (reservedPropertyTypeNames.InvariantContains(propertyType.Alias)) | ||
{ | ||
return ContentTypeOperationStatus.InvalidPropertyTypeAlias; | ||
} | ||
} | ||
|
||
// validate property data types | ||
Guid[] dataTypeKeys = requestModel.Properties.Select(property => property.DataTypeId).ToArray(); | ||
var dataTypesByKey = dataTypeKeys | ||
// FIXME: create GetAllAsync(params Guid[] keys) method on IDataTypeService | ||
.Select(async key => await _dataTypeService.GetAsync(key)) | ||
.Select(t => t.Result) | ||
.WhereNotNull() | ||
.ToDictionary(dataType => dataType.Key); | ||
if (dataTypeKeys.Length != dataTypesByKey.Count()) | ||
{ | ||
return ContentTypeOperationStatus.InvalidDataType; | ||
} | ||
|
||
// filter out properties and containers with no name/alias | ||
requestModel.Properties = requestModel.Properties.Where(propertyType => propertyType.Alias.IsNullOrWhiteSpace() is false).ToArray(); | ||
requestModel.Containers = requestModel.Containers.Where(container => container.Name.IsNullOrWhiteSpace() is false).ToArray(); | ||
|
||
// update basic content type settings | ||
contentType.Alias = requestModel.Alias; | ||
contentType.Description = requestModel.Description; | ||
contentType.Icon = requestModel.Icon; | ||
contentType.IsElement = requestModel.IsElement; | ||
contentType.Name = requestModel.Name; | ||
contentType.AllowedAsRoot = requestModel.AllowedAsRoot; | ||
contentType.SetVariesBy(ContentVariation.Culture, requestModel.VariesByCulture); | ||
contentType.SetVariesBy(ContentVariation.Segment, requestModel.VariesBySegment); | ||
|
||
// update allowed content types | ||
var allowedContentTypesUnchanged = contentType.AllowedContentTypes? | ||
.OrderBy(contentTypeSort => contentTypeSort.SortOrder) | ||
.Select(contentTypeSort => contentTypeSort.Key) | ||
.SequenceEqual(requestModel.AllowedContentTypes | ||
.OrderBy(contentTypeSort => contentTypeSort.SortOrder) | ||
.Select(contentTypeSort => contentTypeSort.Id)) ?? false; | ||
if (allowedContentTypesUnchanged is false) | ||
{ | ||
// need the content type IDs here - yet, anyway - see FIXME in Umbraco.Cms.Core.Models.ContentTypeSort | ||
var allContentTypesByKey = _contentTypeService.GetAll().ToDictionary(c => c.Key); | ||
contentType.AllowedContentTypes = requestModel | ||
.AllowedContentTypes | ||
.Select((contentTypeSort, index) => allContentTypesByKey.TryGetValue(contentTypeSort.Id, out IContentType? ct) | ||
? new ContentTypeSort(new Lazy<int>(() => ct.Id), contentTypeSort.Id, index, ct.Alias) | ||
: null) | ||
.WhereNotNull() | ||
.ToArray(); | ||
} | ||
|
||
// build a dictionary of parent container IDs and their names (we need it when mapping property groups) | ||
var parentContainerNamesById = requestModel | ||
.Containers | ||
.Where(container => container.ParentId is not null) | ||
.DistinctBy(container => container.ParentId) | ||
.ToDictionary( | ||
container => container.ParentId!.Value, | ||
container => requestModel.Containers.First(c => c.Id == container.ParentId).Name!); | ||
|
||
// FIXME: when refactoring for media and member types, this needs to be some kind of abstract implementation - media and member types do not support publishing | ||
const bool supportsPublishing = true; | ||
|
||
// update properties and groups | ||
PropertyGroup[] propertyGroups = requestModel.Containers.Select(container => | ||
{ | ||
PropertyGroup propertyGroup = contentType.PropertyGroups.FirstOrDefault(group => group.Key == container.Id) ?? | ||
new PropertyGroup(supportsPublishing); | ||
// NOTE: eventually group.Type should be a string to make the client more flexible; for now we'll have to parse the string value back to its expected enum | ||
propertyGroup.Type = Enum.Parse<PropertyGroupType>(container.Type); | ||
propertyGroup.Name = container.Name; | ||
// this is not pretty, but this is how the data structure is at the moment; we just have to live with it for the time being. | ||
var alias = container.Name!; | ||
if (container.ParentId is not null) | ||
{ | ||
alias = $"{parentContainerNamesById[container.ParentId.Value]}/{alias}"; | ||
} | ||
propertyGroup.Alias = alias; | ||
propertyGroup.SortOrder = container.SortOrder; | ||
IPropertyType[] properties = requestModel | ||
.Properties | ||
.Where(property => property.ContainerId == container.Id) | ||
.Select(property => | ||
{ | ||
// get the selected data type | ||
// NOTE: this only works because we already ensured that the data type is present in the dataTypesByKey dictionary | ||
IDataType dataType = dataTypesByKey[property.DataTypeId]; | ||
// get the current property type (if it exists) | ||
IPropertyType propertyType = contentType.PropertyTypes.FirstOrDefault(pt => pt.Key == property.Id) | ||
?? new Core.Models.PropertyType(_shortStringHelper, dataType); | ||
propertyType.Name = property.Name; | ||
propertyType.DataTypeId = dataType.Id; | ||
propertyType.DataTypeKey = dataType.Key; | ||
propertyType.Mandatory = property.Validation.Mandatory; | ||
propertyType.MandatoryMessage = property.Validation.MandatoryMessage; | ||
propertyType.ValidationRegExp = property.Validation.RegEx; | ||
propertyType.ValidationRegExpMessage = property.Validation.RegExMessage; | ||
propertyType.SetVariesBy(ContentVariation.Culture, property.VariesByCulture); | ||
propertyType.SetVariesBy(ContentVariation.Segment, property.VariesBySegment); | ||
propertyType.PropertyGroupId = new Lazy<int>(() => propertyGroup.Id, false); | ||
propertyType.Alias = property.Alias; | ||
propertyType.Description = property.Description; | ||
propertyType.SortOrder = property.SortOrder; | ||
propertyType.LabelOnTop = property.Appearance.LabelOnTop; | ||
return propertyType; | ||
}) | ||
.ToArray(); | ||
if (properties.Any() is false && parentContainerNamesById.ContainsKey(container.Id) is false) | ||
{ | ||
// FIXME: if at all possible, retain empty containers (bad DX to remove stuff that's been attempted saved) | ||
return null; | ||
} | ||
if (propertyGroup.PropertyTypes == null || propertyGroup.PropertyTypes.SequenceEqual(properties) is false) | ||
{ | ||
propertyGroup.PropertyTypes = new PropertyTypeCollection(supportsPublishing, properties); | ||
} | ||
return propertyGroup; | ||
}) | ||
.WhereNotNull() | ||
.ToArray(); | ||
|
||
if (contentType.PropertyGroups.SequenceEqual(propertyGroups) is false) | ||
{ | ||
contentType.PropertyGroups = new PropertyGroupCollection(propertyGroups); | ||
} | ||
|
||
// FIXME: handle properties outside containers ("generic properties") if they still exist | ||
// FIXME: handle compositions (yeah, that) | ||
|
||
// update content type history clean-up | ||
contentType.HistoryCleanup ??= new HistoryCleanup(); | ||
contentType.HistoryCleanup.PreventCleanup = requestModel.Cleanup.PreventCleanup; | ||
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = requestModel.Cleanup.KeepAllVersionsNewerThanDays; | ||
contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = requestModel.Cleanup.KeepLatestVersionPerDayForDays; | ||
|
||
// update allowed templates and assign default template | ||
ITemplate[] allowedTemplates = requestModel.AllowedTemplateIds | ||
.Select(async templateId => await _templateService.GetAsync(templateId)) | ||
.Select(t => t.Result) | ||
.WhereNotNull() | ||
.ToArray(); | ||
contentType.AllowedTemplates = allowedTemplates; | ||
// NOTE: incidentally this also covers removing the default template; when requestModel.DefaultTemplateId is null, | ||
// contentType.SetDefaultTemplate() will be called with a null value, which will reset the default template. | ||
contentType.SetDefaultTemplate(allowedTemplates.FirstOrDefault(t => t.Key == requestModel.DefaultTemplateId)); | ||
|
||
// save content type | ||
// FIXME: create and use an async get method here. | ||
_contentTypeService.Save(contentType); | ||
|
||
return ContentTypeOperationStatus.Success; | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
src/Umbraco.Cms.Api.Management/Controllers/DocumentType/DeleteDocumentTypeController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
using Asp.Versioning; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType; | ||
using Umbraco.Cms.Core; | ||
using Umbraco.Cms.Core.Models; | ||
using Umbraco.Cms.Core.Services; | ||
|
||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; | ||
|
||
[ApiVersion("1.0")] | ||
public class DeleteDocumentTypeController : DocumentTypeControllerBase | ||
{ | ||
private readonly IContentTypeService _contentTypeService; | ||
|
||
public DeleteDocumentTypeController(IContentTypeService contentTypeService) | ||
=> _contentTypeService = contentTypeService; | ||
|
||
[HttpDelete("{id:guid}")] | ||
[MapToApiVersion("1.0")] | ||
[ProducesResponseType(typeof(DocumentTypeResponseModel), StatusCodes.Status200OK)] | ||
[ProducesResponseType(StatusCodes.Status404NotFound)] | ||
public async Task<IActionResult> Delete(Guid id) | ||
{ | ||
// FIXME: create and use an async get method here. | ||
IContentType? contentType = _contentTypeService.Get(id); | ||
if (contentType == null) | ||
{ | ||
return NotFound(); | ||
} | ||
|
||
// FIXME: create overload that accepts user key | ||
_contentTypeService.Delete(contentType, Constants.Security.SuperUserId); | ||
return await Task.FromResult(Ok()); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
src/Umbraco.Cms.Api.Management/Controllers/DocumentType/UpdateDocumentTypeController.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
using Asp.Versioning; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Mvc; | ||
using Umbraco.Cms.Api.Management.ViewModels.DocumentType; | ||
using Umbraco.Cms.Core.Models; | ||
using Umbraco.Cms.Core.Services; | ||
using Umbraco.Cms.Core.Services.OperationStatus; | ||
using Umbraco.Cms.Core.Strings; | ||
|
||
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType; | ||
|
||
[ApiVersion("1.0")] | ||
public class UpdateDocumentTypeController : CreateUpdateDocumentTypeControllerBase | ||
{ | ||
private readonly IContentTypeService _contentTypeService; | ||
|
||
public UpdateDocumentTypeController(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IShortStringHelper shortStringHelper, ITemplateService templateService) | ||
: base(contentTypeService, dataTypeService, shortStringHelper, templateService) | ||
=> _contentTypeService = contentTypeService; | ||
|
||
[HttpPut("{id:guid}")] | ||
[MapToApiVersion("1.0")] | ||
[ProducesResponseType(StatusCodes.Status200OK)] | ||
[ProducesResponseType(StatusCodes.Status400BadRequest)] | ||
[ProducesResponseType(StatusCodes.Status404NotFound)] | ||
public async Task<IActionResult> Update(Guid id, UpdateDocumentTypeRequestModel requestModel) | ||
{ | ||
if (requestModel.Compositions.Any()) | ||
{ | ||
return await Task.FromResult(BadRequest("Compositions and inheritance is not yet supported by this endpoint")); | ||
} | ||
|
||
IContentType? contentType = _contentTypeService.Get(id); | ||
if (contentType is null) | ||
{ | ||
return NotFound(); | ||
} | ||
|
||
ContentTypeOperationStatus result = HandleRequest<UpdateDocumentTypeRequestModel, UpdateDocumentTypePropertyTypeRequestModel, UpdateDocumentTypePropertyTypeContainerRequestModel>(contentType, requestModel); | ||
|
||
return result == ContentTypeOperationStatus.Success | ||
? Ok() | ||
: BadRequest(result); | ||
} | ||
} |
Oops, something went wrong.