Skip to content

Commit

Permalink
Add ancestor endpoints and remove explicit parent context (#15746)
Browse files Browse the repository at this point in the history
* Remove explicit parent context in API outputs

* Add ancestor endpoints for document and data type (experimental for now)

* Add ancestor endpoints for doctypes, media, mediatypes, partial views, scripts, static files, stylesheets and templates

* Add unit tests for ancestor ID parsing

* Add ancestor endpoint for dictionary items

* Update OpenApi.json

* Fix merge and regenerate OpenApi.json

* Regenerate OpenApi.json

* Rename "folder" to "parent" for consistency

* Fix merge

* Fix merge

* Include "self" in ancestor endpoints

* Handle ancestors for root items correctly

* Remove "type" from recycle bin items

* Tests against fixed values instead of calculated ones.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
  • Loading branch information
kjac and Sven Geusens committed Mar 25, 2024
1 parent e441639 commit f6f868e
Show file tree
Hide file tree
Showing 24 changed files with 1,307 additions and 52 deletions.
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
{
public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase
{
public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
Expand Up @@ -23,6 +23,6 @@ public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(parentId, skip, take);

return Ok(PagedViewModel(await MapTreeItemViewModels(parentId, paginatedItems.Items), paginatedItems.Total));
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}
Expand Up @@ -27,25 +27,51 @@ public DictionaryTreeControllerBase(IEntityService entityService, IDictionaryIte

protected IDictionaryItemService DictionaryItemService { get; }

protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(Guid? parentKey, IEnumerable<IDictionaryItem> dictionaryItems)
protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(IEnumerable<IDictionaryItem> dictionaryItems)
=> await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));

protected override async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
IDictionaryItem? dictionaryItem = await DictionaryItemService.GetAsync(descendantKey);
if (dictionaryItem is null)
{
// this looks weird - but we actually mimic how the rest of the ancestor (and children) endpoints actually work
return Ok(Enumerable.Empty<NamedEntityTreeItemResponseModel>());
}

var ancestors = new List<IDictionaryItem>();
if (includeSelf)
{
var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0;
return new NamedEntityTreeItemResponseModel
ancestors.Add(dictionaryItem);
}

while (dictionaryItem?.ParentId is not null)
{
dictionaryItem = await DictionaryItemService.GetAsync(dictionaryItem.ParentId.Value);
if (dictionaryItem is not null)
{
Name = dictionaryItem.ItemKey,
Id = dictionaryItem.Key,
HasChildren = hasChildren,
Parent = parentKey.HasValue
? new ReferenceByIdModel
{
Id = parentKey.Value
}
: null
};
ancestors.Add(dictionaryItem);
}
}

return await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));
NamedEntityTreeItemResponseModel[] viewModels = await Task.WhenAll(ancestors.Select(CreateEntityTreeItemViewModelAsync));
return Ok(viewModels.Reverse());
}

private async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
{
var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0;
return new NamedEntityTreeItemResponseModel
{
Name = dictionaryItem.ItemKey,
Id = dictionaryItem.Key,
HasChildren = hasChildren,
Parent = dictionaryItem.ParentId.HasValue
? new ReferenceByIdModel
{
Id = dictionaryItem.ParentId.Value
}
: null
};
}
}
Expand Up @@ -23,6 +23,6 @@ public async Task<ActionResult<PagedViewModel<NamedEntityTreeItemResponseModel>>
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(null, skip, take);

return Ok(PagedViewModel(await MapTreeItemViewModels(null, paginatedItems.Items), paginatedItems.Total));
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}
@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
public AncestorsDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public AncestorsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -0,0 +1,32 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsMediaTreeController : MediaTreeControllerBase
{
public AncestorsMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;

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

[ApiVersion("1.0")]
public class AncestorsMediaTypeTreeController : MediaTypeTreeControllerBase
{
public AncestorsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<MediaTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;

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

[ApiVersion("1.0")]
public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase
{
public AncestorsPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}
Expand Up @@ -15,14 +15,9 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
where TItem : RecycleBinItemResponseModelBase, new()
{
private readonly IEntityService _entityService;
private readonly string _itemUdiType;

protected RecycleBinControllerBase(IEntityService entityService)
{
_entityService = entityService;
// ReSharper disable once VirtualMemberCallInConstructor
_itemUdiType = ItemObjectType.GetUdiType();
}
=> _entityService = entityService;

protected abstract UmbracoObjectTypes ItemObjectType { get; }

Expand Down Expand Up @@ -59,7 +54,6 @@ protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim enti
var viewModel = new TItem
{
Id = entity.Key,
Type = _itemUdiType,
HasChildren = entity.HasChildren,
Parent = parentKey.HasValue
? new ItemReferenceByIdResponseModel
Expand Down
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;

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

[ApiVersion("1.0")]
public class AncestorsScriptTreeController : ScriptTreeControllerBase
{
public AncestorsScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;

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

[ApiVersion("1.0")]
public class AncestorsStaticFileTreeController : StaticFileTreeControllerBase
{
public AncestorsStaticFileTreeController(IPhysicalFileSystem physicalFileSystem)
: base(physicalFileSystem)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;

Expand Down Expand Up @@ -29,7 +30,12 @@ protected override string[] GetFiles(string path)
? Array.Empty<string>()
: base.GetFiles(path);

protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
=> IsAllowedPath(path)
? base.GetAncestorModels(path, includeSelf)
: Array.Empty<FileSystemTreeItemPresentationModel>();

private bool IsTreeRootPath(string path) => string.IsNullOrWhiteSpace(path);

private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}/"));
private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}{Path.DirectorySeparatorChar}"));
}
@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;

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

[ApiVersion("1.0")]
public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase
{
public AncestorsStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}

[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}

0 comments on commit f6f868e

Please sign in to comment.