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

Temporary CRUD API for content types #14334

Merged
merged 5 commits into from Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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);
}
}
@@ -0,0 +1,221 @@
using Umbraco.Cms.Api.Management.ViewModels.ContentType;

Check warning on line 1 in src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ New issue: Complex Method

HandleRequest has a cyclomatic complexity of 22, threshold = 9. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.

Check warning on line 1 in src/Umbraco.Cms.Api.Management/Controllers/DocumentType/CreateUpdateDocumentTypeControllerBase.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v14/dev)

❌ New issue: Bumpy Road Ahead

HandleRequest has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is one single, nested block per function. The Bumpy Road code smell is a function that contains multiple chunks of nested conditional logic. The deeper the nesting and the more bumps, the lower the code health.
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)
{
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;
}
}
@@ -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());
}
}
@@ -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);
}
}
Expand Up @@ -6,12 +6,12 @@

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

public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeResponseModel, TPropertyTypeContainerResponseModel>
public abstract class ContentTypeMapDefinition<TContentType, TPropertyTypeModel, TPropertyTypeContainerModel>
where TContentType : IContentTypeBase
where TPropertyTypeResponseModel : PropertyTypeResponseModelBase, new()
where TPropertyTypeContainerResponseModel : PropertyTypeContainerResponseModelBase, new()
where TPropertyTypeModel : PropertyTypeModelBase, new()
where TPropertyTypeContainerModel : PropertyTypeContainerModelBase, new()
{
protected IEnumerable<TPropertyTypeResponseModel> MapPropertyTypes(TContentType source)
protected IEnumerable<TPropertyTypeModel> MapPropertyTypes(TContentType source)
{
// create a mapping table between properties and their associated groups
var groupKeysByPropertyKeys = source
Expand All @@ -21,7 +21,7 @@ protected IEnumerable<TPropertyTypeResponseModel> MapPropertyTypes(TContentType
.ToDictionary(map => map.PropertyTypeKey, map => map.GroupKey);

return source.PropertyTypes.Select(propertyType =>
new TPropertyTypeResponseModel
new TPropertyTypeModel
{
Id = propertyType.Key,
SortOrder = propertyType.SortOrder,
Expand Down Expand Up @@ -49,7 +49,7 @@ protected IEnumerable<TPropertyTypeResponseModel> MapPropertyTypes(TContentType
.ToArray();
}

protected IEnumerable<TPropertyTypeContainerResponseModel> MapPropertyTypeContainers(TContentType source)
protected IEnumerable<TPropertyTypeContainerModel> MapPropertyTypeContainers(TContentType source)
{
// create a mapping table between property group aliases and keys
var groupKeysByGroupAliases = source
Expand All @@ -67,13 +67,13 @@ protected IEnumerable<TPropertyTypeContainerResponseModel> MapPropertyTypeContai
return source
.PropertyGroups
.Select(propertyGroup =>
new TPropertyTypeContainerResponseModel
new TPropertyTypeContainerModel
{
Id = propertyGroup.Key,
ParentId = ParentGroupKey(propertyGroup),
Type = propertyGroup.Type.ToString(),
SortOrder = propertyGroup.SortOrder,
Name = propertyGroup.Name,
Name = propertyGroup.Name ?? "-",
})
.ToArray();
}
Expand Down