Added management API endpoint, service and repository for retrieval of references from the recycle bin (#18882)

* Added management API endpoint, service and repository for retrieval of references from the recycle bin.

* Update src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Removed unused code.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-04-04 14:52:39 +02:00
committed by GitHub
parent bf74636f26
commit fd77074d57
9 changed files with 350 additions and 8 deletions

View File

@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin;
[ApiVersion("1.0")]
public class ReferencedByDocumentRecycleBinController : DocumentRecycleBinControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByDocumentRecycleBinController(
IEntityService entityService,
IDocumentPresentationFactory documentPresentationFactory,
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: base(entityService, documentPresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a paged list of tracked references for all items in the document recycle bin, so you can see where an item is being used.
/// </summary>
[HttpGet("referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}

View File

@@ -0,0 +1,50 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin;
[ApiVersion("1.0")]
public class ReferencedByMediaRecycleBinController : MediaRecycleBinControllerBase
{
private readonly ITrackedReferencesService _trackedReferencesService;
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
public ReferencedByMediaRecycleBinController(
IEntityService entityService,
IMediaPresentationFactory mediaPresentationFactory,
ITrackedReferencesService trackedReferencesService,
IRelationTypePresentationFactory relationTypePresentationFactory)
: base(entityService, mediaPresentationFactory)
{
_trackedReferencesService = trackedReferencesService;
_relationTypePresentationFactory = relationTypePresentationFactory;
}
/// <summary>
/// Gets a paged list of tracked references for all items in the media recycle bin, so you can see where an item is being used.
/// </summary>
[HttpGet("referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Media, skip, take, true);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}

View File

@@ -10705,6 +10705,61 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/document/referenced-by": {
"get": {
"tags": [
"Document"
],
"operationId": "GetRecycleBinDocumentReferencedBy",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/recycle-bin/document/root": {
"get": {
"tags": [
@@ -17856,6 +17911,61 @@
]
}
},
"/umbraco/management/api/v1/recycle-bin/media/referenced-by": {
"get": {
"tags": [
"Media"
],
"operationId": "GetRecycleBinMediaReferencedBy",
"parameters": [
{
"name": "skip",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "take",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 20
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"$ref": "#/components/schemas/PagedIReferenceResponseModel"
}
]
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
},
"403": {
"description": "The authenticated user does not have access to this resource"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/recycle-bin/media/root": {
"get": {
"tags": [

View File

@@ -70,6 +70,29 @@ public interface ITrackedReferencesRepository
bool filterMustBeIsDependency,
out long totalRecords);
/// <summary>
/// Gets a paged result of items which are in relation with an item in the recycle bin.
/// </summary>
/// <param name="objectTypeKey">The Umbraco object type that has recycle bin support (currently Document or Media).</param>
/// <param name="skip">The amount of items to skip.</param>
/// <param name="take">The amount of items to take.</param>
/// <param name="filterMustBeIsDependency">
/// A boolean indicating whether to filter only the RelationTypes which are
/// dependencies (isDependency field is set to true).
/// </param>
/// <param name="totalRecords">The total count of the items with reference to the current item.</param>
/// <returns>An enumerable list of <see cref="RelationItem" /> objects.</returns>
IEnumerable<RelationItemModel> GetPagedRelationsForRecycleBin(
Guid objectTypeKey,
long skip,
long take,
bool filterMustBeIsDependency,
out long totalRecords)
{
totalRecords = 0;
return [];
}
[Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")]
IEnumerable<RelationItemModel> GetPagedRelationsForItem(
int id,

View File

@@ -64,6 +64,20 @@ public interface ITrackedReferencesService
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
Task<PagedModel<RelationItemModel>> GetPagedRelationsForItemAsync(Guid key, long skip, long take, bool filterMustBeIsDependency);
/// <summary>
/// Gets a paged result of items which are in relation with an item in the recycle bin.
/// </summary>
/// <param name="objectType">The Umbraco object type that has recycle bin support (currently Document or Media).</param>
/// <param name="skip">The amount of items to skip</param>
/// <param name="take">The amount of items to take.</param>
/// <param name="filterMustBeIsDependency">
/// A boolean indicating whether to filter only the RelationTypes which are
/// dependencies (isDependency field is set to true).
/// </param>
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
=> Task.FromResult(new PagedModel<RelationItemModel>(0, []));
[Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")]
PagedModel<RelationItemModel> GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency);

View File

@@ -92,6 +92,21 @@ public class TrackedReferencesService : ITrackedReferencesService
return await Task.FromResult(pagedModel);
}
public async Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
{
Guid objectTypeKey = objectType switch
{
UmbracoObjectTypes.Document => Constants.ObjectTypes.Document,
UmbracoObjectTypes.Media => Constants.ObjectTypes.Media,
_ => throw new ArgumentOutOfRangeException(nameof(objectType), "Only documents and media have recycle bin support."),
};
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
IEnumerable<RelationItemModel> items = _trackedReferencesRepository.GetPagedRelationsForRecycleBin(objectTypeKey, skip, take, filterMustBeIsDependency, out var totalItems);
var pagedModel = new PagedModel<RelationItemModel>(totalItems, items);
return await Task.FromResult(pagedModel);
}
[Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")]
public PagedModel<RelationItemModel> GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency)
{

View File

@@ -1,3 +1,4 @@
using System.Linq.Expressions;
using NPoco;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
@@ -92,8 +93,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
Sql<ISqlContext> innerUnionSqlChild = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
"[cn].uniqueId as [key]", "[pn].uniqueId as otherKey, [cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]",
"[rt].[isDependency]", "[rt].[dual]")
"[cn].uniqueId as [key]",
"[cn].trashed as [trashed]",
"[cn].nodeObjectType as [nodeObjectType]",
"[pn].uniqueId as otherKey," +
"[cr].childId as id",
"[cr].parentId as otherId",
"[rt].[alias]",
"[rt].[name]",
"[rt].[isDependency]",
"[rt].[dual]")
.From<RelationDto>("cr")
.InnerJoin<RelationTypeDto>("rt")
.On<RelationDto, RelationTypeDto>((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt")
@@ -103,8 +112,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.On<RelationDto, NodeDto>((cr, pn) => cr.ParentId == pn.NodeId, "cr", "pn");
Sql<ISqlContext> innerUnionSqlDualParent = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
"[pn].uniqueId as [key]", "[cn].uniqueId as otherKey, [dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]",
"[dprt].[isDependency]", "[dprt].[dual]")
"[pn].uniqueId as [key]",
"[pn].trashed as [trashed]",
"[pn].nodeObjectType as [nodeObjectType]",
"[cn].uniqueId as otherKey," +
"[dpr].parentId as id",
"[dpr].childId as otherId",
"[dprt].[alias]",
"[dprt].[name]",
"[dprt].[isDependency]",
"[dprt].[dual]")
.From<RelationDto>("dpr")
.InnerJoin<RelationTypeDto>("dprt")
.On<RelationDto, RelationTypeDto>(
@@ -115,8 +132,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.On<RelationDto, NodeDto>((dpr, pn) => dpr.ParentId == pn.NodeId, "dpr", "pn");
Sql<ISqlContext> innerUnionSql3 = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
"[cn].uniqueId as [key]", "[pn].uniqueId as otherKey, [dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]",
"[dcrt].[isDependency]", "[dcrt].[dual]")
"[cn].uniqueId as [key]",
"[cn].trashed as [trashed]",
"[cn].nodeObjectType as [nodeObjectType]",
"[pn].uniqueId as otherKey," +
"[dcr].childId as id",
"[dcr].parentId as otherId",
"[dcrt].[alias]",
"[dcrt].[name]",
"[dcrt].[isDependency]",
"[dcrt].[dual]")
.From<RelationDto>("dcr")
.InnerJoin<RelationTypeDto>("dcrt")
.On<RelationDto, RelationTypeDto>(
@@ -277,6 +302,32 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
long take,
bool filterMustBeIsDependency,
out long totalRecords)
=> GetPagedRelations(
x => x.Key == key,
skip,
take,
filterMustBeIsDependency,
out totalRecords);
public IEnumerable<RelationItemModel> GetPagedRelationsForRecycleBin(
Guid objectTypeKey,
long skip,
long take,
bool filterMustBeIsDependency,
out long totalRecords)
=> GetPagedRelations(
x => x.NodeObjectType == objectTypeKey && x.Trashed == true,
skip,
take,
filterMustBeIsDependency,
out totalRecords);
private IEnumerable<RelationItemModel> GetPagedRelations(
Expression<Func<UnionHelperDto, bool>> itemsFilter,
long skip,
long take,
bool filterMustBeIsDependency,
out long totalRecords)
{
Sql<ISqlContext> innerUnionSql = GetInnerUnionSql();
Sql<ISqlContext>? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct(
@@ -315,7 +366,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
(left, right) => left.NodeId == right.NodeId,
aliasLeft: "n",
aliasRight: "d")
.Where<UnionHelperDto>(x => x.Key == key, "x");
.Where(itemsFilter, "x");
if (filterMustBeIsDependency)
@@ -763,6 +814,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
[Column("key")] public Guid Key { get; set; }
[Column("trashed")] public bool Trashed { get; set; }
[Column("nodeObjectType")] public Guid NodeObjectType { get; set; }
[Column("otherKey")] public Guid OtherKey { get; set; }
[Column("alias")] public string? Alias { get; set; }

View File

@@ -134,4 +134,11 @@
<Right>lib/net9.0/Umbraco.Tests.Integration.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
<Suppression>
<DiagnosticId>CP0002</DiagnosticId>
<Target>M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.TrackedReferencesServiceTests.Does_not_return_references_if_item_is_not_referenced</Target>
<Left>lib/net9.0/Umbraco.Tests.Integration.dll</Left>
<Right>lib/net9.0/Umbraco.Tests.Integration.dll</Right>
<IsBaselineSuppression>true</IsBaselineSuppression>
</Suppression>
</Suppressions>

View File

@@ -80,7 +80,7 @@ public class TrackedReferencesServiceTests : UmbracoIntegrationTest
}
[Test]
public async Task Does_not_return_references_if_item_is_not_referenced()
public async Task Does_Not_Return_References_If_Item_Is_Not_Referenced()
{
var sut = GetRequiredService<ITrackedReferencesService>();
@@ -88,4 +88,22 @@ public class TrackedReferencesServiceTests : UmbracoIntegrationTest
Assert.AreEqual(0, actual.Total);
}
[Test]
public async Task Get_Pages_That_Reference_Recycle_Bin_Contents()
{
ContentService.MoveToRecycleBin(Root1);
var sut = GetRequiredService<ITrackedReferencesService>();
var actual = await sut.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, 0, 10, true);
Assert.Multiple(() =>
{
Assert.AreEqual(1, actual.Total);
var item = actual.Items.FirstOrDefault();
Assert.AreEqual(Root2.ContentType.Alias, item?.ContentTypeAlias);
Assert.AreEqual(Root2.Key, item?.NodeKey);
});
}
}