From a5b9fd16503cc38c00b0e4d8c73596d95369e558 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 1 Jul 2024 09:17:48 +0200 Subject: [PATCH 01/38] V15: Update to dotnet 9 (#16625) * Update to dotnet 9 and update nuget packages * Update umbraco code version * Update Directory.Build.props Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Include preview version in pipeline * update template projects * update global json with specific version * Update version.json to v15 * Rename TrimStart and TrimEnd to string specific * Rename to Exact * Update global.json Co-authored-by: Ronald Barendse * Remove includePreviewVersion * Rename to trim exact --------- Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Ronald Barendse --- Directory.Build.props | 2 +- Directory.Packages.props | 46 +++++++++---------- build/azure-pipelines.yml | 2 +- global.json | 2 +- .../OpenApi/SchemaIdHandler.cs | 2 +- .../Services/ApiMediaQueryService.cs | 2 +- .../Services/RequestRedirectService.cs | 2 +- .../Indexer/AllIndexerController.cs | 2 +- .../Searcher/AllSearcherController.cs | 2 +- .../umbraco/UmbracoWebsite/Maintenance.cshtml | 2 +- .../umbraco/UmbracoWebsite/NoNodes.cshtml | 2 +- .../umbraco/UmbracoWebsite/NotFound.cshtml | 2 +- .../ModelsBuilderConfigExtensions.cs | 2 +- .../DeliveryApi/ApiContentRouteBuilder.cs | 2 +- .../Extensions/StringExtensions.cs | 10 ++-- src/Umbraco.Core/Models/Content.cs | 12 ++--- src/Umbraco.Core/Models/ContentBase.cs | 12 ++--- .../Routing/UmbracoRequestPaths.cs | 4 +- .../BackOfficeExamineSearcher.cs | 2 +- .../LuceneIndexDiagnostics.cs | 2 +- .../DeliveryApi/ApiRichTextElementParser.cs | 2 +- .../Install/FilePermissionHelper.cs | 4 +- .../Logging/Serilog/LoggerConfigExtensions.cs | 2 +- .../Persistence/NPocoSqlExtensions.cs | 4 +- .../Repositories/Implement/UserRepository.cs | 2 +- .../SqlSyntax/SqlSyntaxProviderBase.cs | 2 +- .../ApplicationBuilderExtensions.cs | 2 +- .../Extensions/LinkGeneratorExtensions.cs | 2 +- .../Extensions/UrlHelperExtensions.cs | 4 +- .../.template.config/template.json | 10 ++-- .../UmbracoPackage/UmbracoPackage.csproj | 2 +- .../.template.config/template.json | 10 ++-- .../UmbracoPackageRcl/UmbracoPackage.csproj | 2 +- .../.template.config/template.json | 10 ++-- .../UmbracoProject/UmbracoProject.csproj | 2 +- tests/Directory.Packages.props | 8 ++-- .../UmbracoBuilderExtensions.cs | 2 +- .../ManagementApi/ManagementApiTest.cs | 2 +- .../ExamineExternalIndexSearcherTest.cs | 2 +- .../StringExtensionsTests.cs | 4 +- .../NPocoTests/NPocoSqlTemplateTests.cs | 2 +- .../Umbraco.JsonSchema.csproj | 2 +- version.json | 2 +- 43 files changed, 99 insertions(+), 99 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5f3055125f..9019dfa3da 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - net8.0 + net9.0 Umbraco HQ Umbraco Copyright © Umbraco $([System.DateTime]::Today.ToString('yyyy')) diff --git a/Directory.Packages.props b/Directory.Packages.props index 090afcd216..c652e5b7ed 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,32 +7,32 @@ - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -83,7 +83,7 @@ - + diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index aa7e1845f7..7d89c8b97f 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -528,7 +528,7 @@ stages: - ${{ if eq(parameters.isNightly, true) }}: pwsh: npm run test --ignore-certificate-errors ${{ else }}: - pwsh: npm run smokeTest --ignore-certificate-errors + pwsh: npm run smokeTest --ignore-certificate-errors displayName: Run Playwright tests continueOnError: true workingDirectory: tests/Umbraco.Tests.AcceptanceTest diff --git a/global.json b/global.json index 391ba3c2a3..f1d8f700f5 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100-preview.5.24307.3", "rollForward": "latestFeature" } } diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs index c08e0be19a..6e96cbed42 100644 --- a/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs +++ b/src/Umbraco.Cms.Api.Common/OpenApi/SchemaIdHandler.cs @@ -36,7 +36,7 @@ public class SchemaIdHandler : ISchemaIdHandler // first grab the "non-generic" part of any generic type name (i.e. "PagedViewModel`1" becomes "PagedViewModel") .Split('`').First() // then remove the "ViewModel" postfix from type names - .TrimEnd("ViewModel"); + .TrimEndExact("ViewModel"); private string HandleGenerics(string name, Type type) { diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs index 5969e0a788..8a078d7f0d 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs @@ -85,7 +85,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService return null; } - var childrenOf = fetch.TrimStart(childrenOfParameter); + var childrenOf = fetch.TrimStartExact(childrenOfParameter); if (childrenOf.IsNullOrWhiteSpace()) { // this mirrors the current behavior of the Content Delivery API :-) diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs index 882525c8d0..4b9efc03a5 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestRedirectService.cs @@ -66,7 +66,7 @@ internal sealed class RequestRedirectService : RoutingServiceBase, IRequestRedir } // important: redirect URLs are always tracked without trailing slashes - IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEnd("/"), culture); + IRedirectUrl? redirectUrl = _redirectUrlService.GetMostRecentRedirectUrl(requestedPath.TrimEndExact("/"), culture); IPublishedContent? content = redirectUrl != null ? _apiPublishedContentCache.GetById(redirectUrl.ContentKey) : null; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs index 768389b56d..6ce4d3b12b 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs @@ -37,7 +37,7 @@ public class AllIndexerController : IndexerControllerBase { IndexResponseModel[] indexes = _examineManager.Indexes .Select(_indexPresentationFactory.Create) - .OrderBy(indexModel => indexModel.Name.TrimEnd("Indexer")).ToArray(); + .OrderBy(indexModel => indexModel.Name.TrimEndExact("Indexer")).ToArray(); var viewModel = new PagedViewModel { Items = indexes.Skip(skip).Take(take), Total = indexes.Length }; return Task.FromResult(viewModel); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs index 76cd2f639b..4c5189dcaa 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Searcher/AllSearcherController.cs @@ -30,7 +30,7 @@ public class AllSearcherController : SearcherControllerBase var searchers = new List( _examineManager.RegisteredSearchers.Select(searcher => new SearcherResponse { Name = searcher.Name }) .OrderBy(x => - x.Name.TrimEnd("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists + x.Name.TrimEndExact("Searcher"))); // order by name , but strip the "Searcher" from the end if it exists var viewModel = new PagedViewModel { Items = searchers.Skip(skip).Take(take), diff --git a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml index 94de5f3c52..5c41abf6fa 100644 --- a/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml +++ b/src/Umbraco.Cms.StaticAssets/umbraco/UmbracoWebsite/Maintenance.cshtml @@ -17,7 +17,7 @@ Website is Under Maintainance - + @await Html.BackOfficeImportMapScriptAsync(JsonSerializer, BackOfficePathGenerator, PackageManifestService) - + From 2a6b376f0deeb132bf52df1df400888b69fd977c Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:03:41 +0200 Subject: [PATCH 07/38] V15 QA fix e2e build pipeline (#16987) * Added a line to check the dotnet version * Moved * Updated dotnet version * Removed version output --- templates/UmbracoProject/.template.config/template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json index ad802476b3..71499fd539 100644 --- a/templates/UmbracoProject/.template.config/template.json +++ b/templates/UmbracoProject/.template.config/template.json @@ -116,7 +116,7 @@ "cases": [ { "condition": "(true)", - "value": "net8.0" + "value": "net9.0" } ] } From 874055eeabec9d893e095cd85cc02ee0955ba267 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 3 Sep 2024 10:43:09 +0200 Subject: [PATCH 08/38] Add member "kind" - and refactor user "type" to "kind" for consistency (#16979) * Rename UserType to UserKind * Add MemberKind to tell API members from regular ones * Remove user kind from invite user endpoint --------- Co-authored-by: Mads Rasmussen --- .../MemberBuilderExtensions.cs | 2 +- .../Factories/MemberPresentationFactory.cs | 34 +++++++++++- .../Factories/UserPresentationFactory.cs | 6 +- .../Mapping/Member/MemberMapDefinition.cs | 2 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 55 +++++++++++-------- .../Member/Item/MemberItemResponseModel.cs | 3 + .../ViewModels/Member/MemberResponseModel.cs | 3 + .../ViewModels/User/CreateUserRequestModel.cs | 6 +- .../User/CreateUserRequestModelBase.cs | 6 ++ .../ViewModels/User/InviteUserRequestModel.cs | 2 +- .../User/Item/UserItemResponseModel.cs | 2 +- .../ViewModels/User/UserResponseModel.cs | 2 +- src/Umbraco.Core/Models/Membership/IUser.cs | 2 +- .../Models/Membership/MemberKind.cs | 7 +++ src/Umbraco.Core/Models/Membership/User.cs | 8 +-- .../Membership/{UserType.cs => UserKind.cs} | 2 +- src/Umbraco.Core/Models/UserCreateModel.cs | 2 +- src/Umbraco.Core/Services/UserService.cs | 6 +- .../Migrations/Upgrade/UmbracoPlan.cs | 2 +- .../{AddTypeToUser.cs => AddKindToUser.cs} | 8 +-- .../Persistence/Dtos/UserDto.cs | 4 +- .../Persistence/Factories/UserFactory.cs | 4 +- .../Security/BackOfficeIdentityUser.cs | 12 ++-- .../Security/BackOfficeUserStore.cs | 2 +- .../Security/IdentityMapDefinition.cs | 2 +- .../Security/BackOfficeUserManager.cs | 2 +- .../UserServiceCrudTests.ChangePassword.cs | 2 +- .../Services/UserServiceCrudTests.Create.cs | 14 ++--- 28 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs create mode 100644 src/Umbraco.Core/Models/Membership/MemberKind.cs rename src/Umbraco.Core/Models/Membership/{UserType.cs => UserKind.cs} (79%) rename src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/{AddTypeToUser.cs => AddKindToUser.cs} (97%) diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs index 90ea476c18..b5a5bb842c 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/MemberBuilderExtensions.cs @@ -10,7 +10,7 @@ internal static class MemberBuilderExtensions { internal static IUmbracoBuilder AddMember(this IUmbracoBuilder builder) { - builder.Services.AddTransient(); + builder.Services.AddSingleton(); builder.Services.AddTransient(); builder.WithCollectionBuilder().Add(); diff --git a/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs index 7e53676c5f..aaa4240caa 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/MemberPresentationFactory.cs @@ -1,7 +1,9 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.Member; using Umbraco.Cms.Api.Management.ViewModels.Member.Item; using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Extensions; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; @@ -19,19 +21,23 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory private readonly IMemberTypeService _memberTypeService; private readonly ITwoFactorLoginService _twoFactorLoginService; private readonly IMemberGroupService _memberGroupService; + private readonly DeliveryApiSettings _deliveryApiSettings; + private IEnumerable? _clientCredentialsMemberKeys; public MemberPresentationFactory( IUmbracoMapper umbracoMapper, IMemberService memberService, IMemberTypeService memberTypeService, ITwoFactorLoginService twoFactorLoginService, - IMemberGroupService memberGroupService) + IMemberGroupService memberGroupService, + IOptions deliveryApiSettings) { _umbracoMapper = umbracoMapper; _memberService = memberService; _memberTypeService = memberTypeService; _twoFactorLoginService = twoFactorLoginService; _memberGroupService = memberGroupService; + _deliveryApiSettings = deliveryApiSettings.Value; } public async Task CreateResponseModelAsync(IMember member, IUser currentUser) @@ -39,6 +45,7 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory MemberResponseModel responseModel = _umbracoMapper.Map(member)!; responseModel.IsTwoFactorEnabled = await _twoFactorLoginService.IsTwoFactorEnabledAsync(member.Key); + responseModel.Kind = GetMemberKind(member.Key); IEnumerable roles = _memberService.GetAllRoles(member.Username); // Get the member groups per role, so we can return the group keys @@ -71,7 +78,8 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory { Id = entity.Key, MemberType = _umbracoMapper.Map(entity)!, - Variants = CreateVariantsItemResponseModels(entity) + Variants = CreateVariantsItemResponseModels(entity), + Kind = GetMemberKind(entity.Key) }; private static IEnumerable CreateVariantsItemResponseModels(ITreeEntity entity) @@ -108,4 +116,24 @@ internal sealed class MemberPresentationFactory : IMemberPresentationFactory return responseModel; } + + private MemberKind GetMemberKind(Guid key) + { + if (_clientCredentialsMemberKeys is null) + { + IEnumerable clientCredentialsMemberUserNames = _deliveryApiSettings + .MemberAuthorization? + .ClientCredentialsFlow? + .AssociatedMembers + .Select(m => m.UserName).ToArray() + ?? []; + + _clientCredentialsMemberKeys = clientCredentialsMemberUserNames + .Select(_memberService.GetByUsername) + .WhereNotNull() + .Select(m => m.Key).ToArray(); + } + + return _clientCredentialsMemberKeys.Contains(key) ? MemberKind.Api : MemberKind.Default; + } } diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs index ccb12a579a..7ba1229416 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserPresentationFactory.cs @@ -80,7 +80,7 @@ public class UserPresentationFactory : IUserPresentationFactory LastLockoutDate = user.LastLockoutDate, LastPasswordChangeDate = user.LastPasswordChangeDate, IsAdmin = user.IsAdmin(), - Type = user.Type + Kind = user.Kind }; return responseModel; @@ -93,7 +93,7 @@ public class UserPresentationFactory : IUserPresentationFactory Name = user.Name ?? user.Username, AvatarUrls = user.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator) .Select(url => _absoluteUrlBuilder.ToAbsoluteUrl(url).ToString()), - Type = user.Type + Kind = user.Kind }; public async Task CreateCreationModelAsync(CreateUserRequestModel requestModel) @@ -105,7 +105,7 @@ public class UserPresentationFactory : IUserPresentationFactory Name = requestModel.Name, UserName = requestModel.UserName, UserGroupKeys = requestModel.UserGroupIds.Select(x => x.Id).ToHashSet(), - Type = requestModel.Type + Kind = requestModel.Kind }; return await Task.FromResult(createModel); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs index 2310da80fa..a6e16c8c7b 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs @@ -17,7 +17,7 @@ public class MemberMapDefinition : ContentMapDefinition mapper.Define((_, _) => new MemberResponseModel(), Map); - // Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups + // Umbraco.Code.MapAll -IsTwoFactorEnabled -Groups -Kind private void Map(IMember source, MemberResponseModel target, MapperContext context) { target.Id = source.Key; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 9f56d0c8aa..1bcf48be97 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -35493,8 +35493,8 @@ "CreateUserRequestModel": { "required": [ "email", + "kind", "name", - "type", "userGroupIds", "userName" ], @@ -35525,8 +35525,8 @@ "format": "uuid", "nullable": true }, - "type": { - "$ref": "#/components/schemas/UserTypeModel" + "kind": { + "$ref": "#/components/schemas/UserKindModel" } }, "additionalProperties": false @@ -38244,7 +38244,6 @@ "required": [ "email", "name", - "type", "userGroupIds", "userName" ], @@ -38275,9 +38274,6 @@ "format": "uuid", "nullable": true }, - "type": { - "$ref": "#/components/schemas/UserTypeModel" - }, "message": { "type": "string", "nullable": true @@ -39415,6 +39411,7 @@ "MemberItemResponseModel": { "required": [ "id", + "kind", "memberType", "variants" ], @@ -39440,10 +39437,20 @@ } ] } + }, + "kind": { + "$ref": "#/components/schemas/MemberKindModel" } }, "additionalProperties": false }, + "MemberKindModel": { + "enum": [ + "Default", + "Api" + ], + "type": "string" + }, "MemberResponseModel": { "required": [ "email", @@ -39453,6 +39460,7 @@ "isApproved", "isLockedOut", "isTwoFactorEnabled", + "kind", "memberType", "username", "values", @@ -39531,6 +39539,9 @@ "type": "string", "format": "uuid" } + }, + "kind": { + "$ref": "#/components/schemas/MemberKindModel" } }, "additionalProperties": false @@ -45082,8 +45093,8 @@ "required": [ "avatarUrls", "id", - "name", - "type" + "kind", + "name" ], "type": "object", "properties": { @@ -45100,12 +45111,19 @@ "type": "string" } }, - "type": { - "$ref": "#/components/schemas/UserTypeModel" + "kind": { + "$ref": "#/components/schemas/UserKindModel" } }, "additionalProperties": false }, + "UserKindModel": { + "enum": [ + "Default", + "Api" + ], + "type": "string" + }, "UserOrderModel": { "enum": [ "UserName", @@ -45172,10 +45190,10 @@ "hasMediaRootAccess", "id", "isAdmin", + "kind", "mediaStartNodeIds", "name", "state", - "type", "updateDate", "userGroupIds", "userName" @@ -45277,8 +45295,8 @@ "isAdmin": { "type": "boolean" }, - "type": { - "$ref": "#/components/schemas/UserTypeModel" + "kind": { + "$ref": "#/components/schemas/UserKindModel" } }, "additionalProperties": false @@ -45339,13 +45357,6 @@ }, "additionalProperties": false }, - "UserTypeModel": { - "enum": [ - "Default", - "Api" - ], - "type": "string" - }, "VariantItemResponseModel": { "required": [ "name" @@ -45566,4 +45577,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs index f959a37a90..e55ad04572 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/Item/MemberItemResponseModel.cs @@ -1,6 +1,7 @@ using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.Item; using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models.Membership; namespace Umbraco.Cms.Api.Management.ViewModels.Member.Item; @@ -9,4 +10,6 @@ public class MemberItemResponseModel : ItemResponseModelBase public MemberTypeReferenceResponseModel MemberType { get; set; } = new(); public IEnumerable Variants { get; set; } = Enumerable.Empty(); + + public MemberKind Kind { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs index 0125d85523..df5f9c9a22 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs @@ -1,5 +1,6 @@ using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.MemberType; +using Umbraco.Cms.Core.Models.Membership; namespace Umbraco.Cms.Api.Management.ViewModels.Member; @@ -26,4 +27,6 @@ public class MemberResponseModel : ContentResponseModelBase Groups { get; set; } = []; + + public MemberKind Kind { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs index dff46bf2fc..3ce44784b9 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModel.cs @@ -2,9 +2,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.User; -public class CreateUserRequestModel : UserPresentationBase +public class CreateUserRequestModel : CreateUserRequestModelBase { - public Guid? Id { get; set; } - - public UserType Type { get; set; } + public UserKind Kind { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs new file mode 100644 index 0000000000..35ccf445ac --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/CreateUserRequestModelBase.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.User; + +public class CreateUserRequestModelBase : UserPresentationBase +{ + public Guid? Id { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs index 9f73d12dea..e41c4d5487 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/InviteUserRequestModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.User; -public class InviteUserRequestModel : CreateUserRequestModel +public class InviteUserRequestModel : CreateUserRequestModelBase { public string? Message { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs index b65b3302df..4dd8139ba1 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/Item/UserItemResponseModel.cs @@ -7,5 +7,5 @@ public class UserItemResponseModel : NamedItemResponseModelBase { public IEnumerable AvatarUrls { get; set; } = Enumerable.Empty(); - public UserType Type { get; set; } + public UserKind Kind { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs index 52c37da7a3..8177b02d1e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/User/UserResponseModel.cs @@ -34,5 +34,5 @@ public class UserResponseModel : UserPresentationBase public bool IsAdmin { get; set; } - public UserType Type { get; set; } + public UserKind Kind { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 3d499f835f..868daee426 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -42,7 +42,7 @@ public interface IUser : IMembershipUser, IRememberBeingDirty /// /// The type of user. /// - UserType Type { get; set; } + UserKind Kind { get; set; } void RemoveGroup(string group); diff --git a/src/Umbraco.Core/Models/Membership/MemberKind.cs b/src/Umbraco.Core/Models/Membership/MemberKind.cs new file mode 100644 index 0000000000..640abcbac1 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/MemberKind.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Models.Membership; + +public enum MemberKind +{ + Default = 0, + Api +} diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 52f91afb8a..b51d207aa3 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -41,7 +41,7 @@ public class User : EntityBase, IUser, IProfile private HashSet _userGroups; private string _username; - private UserType _type; + private UserKind _kind; /// /// Constructor for creating a new/empty user @@ -359,10 +359,10 @@ public class User : EntityBase, IUser, IProfile } [DataMember] - public UserType Type + public UserKind Kind { - get => _type; - set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + get => _kind; + set => SetPropertyValueAndDetectChanges(value, ref _kind, nameof(Kind)); } /// diff --git a/src/Umbraco.Core/Models/Membership/UserType.cs b/src/Umbraco.Core/Models/Membership/UserKind.cs similarity index 79% rename from src/Umbraco.Core/Models/Membership/UserType.cs rename to src/Umbraco.Core/Models/Membership/UserKind.cs index beccd157fd..46cc0edb21 100644 --- a/src/Umbraco.Core/Models/Membership/UserType.cs +++ b/src/Umbraco.Core/Models/Membership/UserKind.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Core.Models.Membership; -public enum UserType +public enum UserKind { Default = 0, Api diff --git a/src/Umbraco.Core/Models/UserCreateModel.cs b/src/Umbraco.Core/Models/UserCreateModel.cs index 556fad5b15..f21561657f 100644 --- a/src/Umbraco.Core/Models/UserCreateModel.cs +++ b/src/Umbraco.Core/Models/UserCreateModel.cs @@ -12,7 +12,7 @@ public class UserCreateModel public string Name { get; set; } = string.Empty; - public UserType Type { get; set; } + public UserKind Kind { get; set; } public ISet UserGroupKeys { get; set; } = new HashSet(); } diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index 6397e96dd0..008e945738 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -1189,7 +1189,7 @@ internal class UserService : RepositoryService, IUserService return Attempt.FailWithStatus(UserOperationStatus.UserNotFound, new PasswordChangedModel()); } - if (user.Type != UserType.Default) + if (user.Kind != UserKind.Default) { return Attempt.FailWithStatus(UserOperationStatus.InvalidUserType, new PasswordChangedModel()); } @@ -2494,7 +2494,7 @@ internal class UserService : RepositoryService, IUserService } IUser? user = await GetAsync(userKey); - if (user is null || user.Type != UserType.Api) + if (user is null || user.Kind != UserKind.Api) { return UserClientCredentialsOperationStatus.InvalidUser; } @@ -2517,7 +2517,7 @@ internal class UserService : RepositoryService, IUserService using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); IUser? user = _userRepository.GetByClientId(clientId); - return Task.FromResult(user?.Type == UserType.Api ? user : null); + return Task.FromResult(user?.Kind == UserKind.Api ? user : null); } public async Task> GetClientIdsAsync(Guid userKey) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 54a73f7839..7cd6663f63 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -94,6 +94,6 @@ public class UmbracoPlan : MigrationPlan // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); - To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); + To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddTypeToUser.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs similarity index 97% rename from src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddTypeToUser.cs rename to src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs index 4882813ce7..b807c4cc9a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddTypeToUser.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddKindToUser.cs @@ -8,12 +8,12 @@ using Umbraco.Cms.Infrastructure.Scoping; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; [Obsolete("Remove in Umbraco 18.")] -public class AddTypeToUser : UnscopedMigrationBase +public class AddKindToUser : UnscopedMigrationBase { - private const string NewColumnName = "type"; + private const string NewColumnName = "kind"; private readonly IScopeProvider _scopeProvider; - public AddTypeToUser(IMigrationContext context, IScopeProvider scopeProvider) + public AddKindToUser(IMigrationContext context, IScopeProvider scopeProvider) : base(context) => _scopeProvider = scopeProvider; @@ -90,7 +90,7 @@ public class AddTypeToUser : UnscopedMigrationBase CreateDate = x.CreateDate, UpdateDate = x.UpdateDate, Avatar = x.Avatar, - Type = 0 + Kind = 0 }); Delete.Table(Constants.DatabaseSchema.Tables.User).Do(); diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs index ea892d8d47..fa8011be29 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserDto.cs @@ -103,10 +103,10 @@ public class UserDto [Constraint(Default = SystemMethods.CurrentDateTime)] public DateTime UpdateDate { get; set; } = DateTime.Now; - [Column("type")] + [Column("kind")] [NullSetting(NullSetting = NullSettings.NotNull)] [Constraint(Default = 0)] - public short Type { get; set; } + public short Kind { get; set; } /// /// Will hold the media file system relative path of the users custom avatar if they uploaded one diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs index 662b2f2e3f..9a8ae11386 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserFactory.cs @@ -47,7 +47,7 @@ internal static class UserFactory user.Avatar = dto.Avatar; user.EmailConfirmedDate = dto.EmailConfirmedDate; user.InvitedDate = dto.InvitedDate; - user.Type = (UserType)dto.Type; + user.Kind = (UserKind)dto.Kind; // reset dirty initial properties (U4-1946) user.ResetDirtyProperties(false); @@ -83,7 +83,7 @@ internal static class UserFactory Avatar = entity.Avatar, EmailConfirmedDate = entity.EmailConfirmedDate, InvitedDate = entity.InvitedDate, - Type = (short)entity.Type + Kind = (short)entity.Kind }; if (entity.StartContentIds is not null) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs index 061195e187..7fdbb043da 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeIdentityUser.cs @@ -21,7 +21,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser private DateTime? _inviteDateUtc; private int[] _startContentIds; private int[] _startMediaIds; - private UserType _type; + private UserKind _kind; /// /// Initializes a new instance of the class. @@ -116,10 +116,10 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser } } - public UserType Type + public UserKind Kind { - get => _type; - set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type)); + get => _kind; + set => BeingDirty.SetPropertyValueAndDetectChanges(value, ref _kind, nameof(Kind)); } /// @@ -130,7 +130,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser /// This is allowed to be null (but would need to be filled in if trying to persist this instance) /// /// - public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null, UserType type = UserType.Default) + public static BackOfficeIdentityUser CreateNew(GlobalSettings globalSettings, string? username, string email, string culture, string? name = null, Guid? id = null, UserKind kind = UserKind.Default) { if (string.IsNullOrWhiteSpace(username)) { @@ -156,7 +156,7 @@ public class BackOfficeIdentityUser : UmbracoIdentityUser user.HasIdentity = false; user._culture = culture; user.Name = name; - user.Type = type; + user.Kind = kind; user.EnableChangeTracking(); return user; } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index b68b3f2189..03d1c35c56 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -141,7 +141,7 @@ public class BackOfficeUserStore : StartMediaIds = user.StartMediaIds ?? new int[] { }, IsLockedOut = user.IsLockedOut, Key = user.Key, - Type = user.Type + Kind = user.Kind }; diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 6731ae1fce..5032a2a871 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -96,7 +96,7 @@ public class IdentityMapDefinition : IMapDefinition target.SecurityStamp = source.SecurityStamp; DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes); target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; - target.Type = source.Type; + target.Kind = source.Kind; } // Umbraco.Code.MapAll -Id -LockoutEnabled -PhoneNumber -PhoneNumberConfirmed -ConcurrencyStamp -NormalizedEmail -NormalizedUserName -Roles diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 31b3963bb9..fc8d4d23e7 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -299,7 +299,7 @@ public class BackOfficeUserManager : UmbracoUserManager { userGroup.Key }, - Type = UserType.Api + Kind = UserKind.Api }; var userKey = (await userService.CreateAsync(Constants.Security.SuperUserKey, creationModel, true)).Result.CreatedUser!.Key; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Create.cs index aaa2f619ec..c70a883179 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserServiceCrudTests.Create.cs @@ -50,12 +50,12 @@ public partial class UserServiceCrudTests Assert.IsNotNull(createdUser); Assert.AreEqual(username, createdUser.Username); Assert.AreEqual(email, createdUser.Email); - Assert.AreEqual(UserType.Default, createdUser.Type); + Assert.AreEqual(UserKind.Default, createdUser.Kind); } - [TestCase(UserType.Default)] - [TestCase(UserType.Api)] - public async Task Can_Create_All_User_Types(UserType type) + [TestCase(UserKind.Default)] + [TestCase(UserKind.Api)] + public async Task Can_Create_All_User_Types(UserKind kind) { var securitySettings = new SecuritySettings(); var userService = CreateUserService(securitySettings); @@ -67,7 +67,7 @@ public partial class UserServiceCrudTests Email = "api@local", Name = "API user", UserGroupKeys = new HashSet { userGroup.Key }, - Type = type + Kind = kind }; var result = await userService.CreateAsync(Constants.Security.SuperUserKey, creationModel, true); @@ -76,11 +76,11 @@ public partial class UserServiceCrudTests Assert.AreEqual(UserOperationStatus.Success, result.Status); var createdUser = result.Result.CreatedUser; Assert.IsNotNull(createdUser); - Assert.AreEqual(type, createdUser.Type); + Assert.AreEqual(kind, createdUser.Kind); var user = await userService.GetAsync(createdUser.Key); Assert.NotNull(user); - Assert.AreEqual(type, user.Type); + Assert.AreEqual(kind, user.Kind); } [Test] From eff520c7eb5ba3e7992accccee31d56335d2bdd1 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 4 Sep 2024 08:31:50 +0200 Subject: [PATCH 09/38] Ensure correct OpenID Connect error responses (#16982) --- .../Controllers/Security/MemberController.cs | 40 +++++++++++++++---- .../Security/BackOfficeController.cs | 25 +++++++++--- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs index f7296a950f..a5b085073b 100644 --- a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs +++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs @@ -79,19 +79,28 @@ public class MemberController : DeliveryApiControllerBase // the Authorize endpoint is not allowed unless authorization code flow is enabled. if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true) { - return BadRequest("Member authorization is not allowed."); + return BadRequest(new OpenIddictResponse + { + Error = "Not allowed", ErrorDescription = "Member authorization is not allowed." + }); } OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest(); if (request is null) { - return BadRequest("Unable to obtain OpenID data from the current request."); + return BadRequest(new OpenIddictResponse + { + Error = "No context found", ErrorDescription = "Unable to obtain context from the current request." + }); } // make sure this endpoint ONLY handles member authentication if (request.ClientId is not Constants.OAuthClientIds.Member) { - return BadRequest("The specified client ID cannot be used here."); + return BadRequest(new OpenIddictResponse + { + Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid." + }); } return request.IdentityProvider.IsNullOrWhiteSpace() @@ -106,7 +115,10 @@ public class MemberController : DeliveryApiControllerBase OpenIddictRequest? request = HttpContext.GetOpenIddictServerRequest(); if (request is null) { - return BadRequest("Unable to obtain OpenID data from the current request."); + return BadRequest(new OpenIddictResponse + { + Error = "No context found", ErrorDescription = "Unable to obtain context from the current request." + }); } // authorization code flow or refresh token flow? @@ -117,7 +129,10 @@ public class MemberController : DeliveryApiControllerBase return authenticateResult is { Succeeded: true, Principal: not null } ? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal) - : BadRequest("The supplied authorization was not be verified."); + : BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified." + }); } // client credentials flow? @@ -128,7 +143,10 @@ public class MemberController : DeliveryApiControllerBase MemberIdentityUser? member = await _memberClientCredentialsManager.FindMemberAsync(request.ClientId!); return member is not null ? await SignInMember(member, request) - : BadRequest("Invalid client or client configuration."); + : BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "Invalid 'client_id' or client configuration." + }); } throw new InvalidOperationException("The requested grant type is not supported."); @@ -159,7 +177,10 @@ public class MemberController : DeliveryApiControllerBase if (member is null) { _logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName); - return BadRequest("The member could not be found."); + return BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found." + }); } return await SignInMember(member, request); @@ -187,7 +208,10 @@ public class MemberController : DeliveryApiControllerBase if (member is null) { _logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager"); - return BadRequest("The member could not be found."); + return BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "The member associated with the supplied 'client_id' could not be found." + }); } // update member authentication tokens if succeeded diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs index 217890a539..b948c034da 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Security/BackOfficeController.cs @@ -169,13 +169,19 @@ public class BackOfficeController : SecurityControllerBase OpenIddictRequest? request = context.GetOpenIddictServerRequest(); if (request == null) { - return BadRequest("Unable to obtain OpenID data from the current request"); + return BadRequest(new OpenIddictResponse + { + Error = "No context found", ErrorDescription = "Unable to obtain context from the current request." + }); } // make sure we keep member authentication away from this endpoint if (request.ClientId is Constants.OAuthClientIds.Member) { - return BadRequest("The specified client ID cannot be used here."); + return BadRequest(new OpenIddictResponse + { + Error = "Invalid 'client ID'", ErrorDescription = "The specified 'client_id' is not valid." + }); } return request.IdentityProvider.IsNullOrWhiteSpace() @@ -192,7 +198,10 @@ public class BackOfficeController : SecurityControllerBase OpenIddictRequest? request = context.GetOpenIddictServerRequest(); if (request == null) { - return BadRequest("Unable to obtain OpenID data from the current request"); + return BadRequest(new OpenIddictResponse + { + Error = "No context found", ErrorDescription = "Unable to obtain context from the current request." + }); } if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) @@ -202,7 +211,10 @@ public class BackOfficeController : SecurityControllerBase return authenticateResult is { Succeeded: true, Principal: not null } ? new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, authenticateResult.Principal) - : BadRequest("The supplied authorization could not be verified."); + : BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "The supplied authorization could not be verified." + }); } if (request.IsClientCredentialsGrantType()) @@ -223,7 +235,10 @@ public class BackOfficeController : SecurityControllerBase // if this happens, the OpenIddict applications have somehow gone out of sync with the Umbraco users _logger.LogError("The user associated with the client ID ({clientId}) could not be found", request.ClientId); - return BadRequest("The user associated with the client ID could not be found"); + return BadRequest(new OpenIddictResponse + { + Error = "Authorization failed", ErrorDescription = "The user associated with the supplied 'client_id' could not be found." + }); } throw new InvalidOperationException("The requested grant type is not supported."); From 5a7d563b8a3fa08b7720525f8969c2094cb1233c Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:18:08 +0200 Subject: [PATCH 10/38] Introduce `INavigationService` for in-memory navigation data (#16818) * Tests * Remove props and use local vars * Adding preliminary navigation service and content implementation * Adding preliminary unit tests * Change from async methods * Refactor GetParentKey to TryGetParentKey * Refactor GetChildrenKeys to TryGetChildrenKeys * Refactor GetDescendantsKeys to TryGetDescendantsKeys * Refactor GetAncestorsKeys to TryGetAncestorsKeys * Refactor GetSiblingsKeys to TryGetSiblingsKeys * Refactor TryGetChildrenKeys * Initial integration tests * Use ContentEditingService instead of ContentService * Remove INavigationService.Copy implementation and unit tests * Rename var * Adding clarification * Initial ContentNavigationRepository * Initial NavigationFactory * Remove filtering from factory * NavigationRepository and implementation * InitializationService responsible for seeding the in-memory structure * Register repository and service * Adding NavigationDto and NavigationNode * Adding INavigationService dependency and Enlist updating navigation structure actions * Documentation * Adding tests for removing descendants as well * Changed to ConcurrentDictionary * Remove keys comments for tests * Adding documentation * Forgotten ConcurrentDictionary change * Isolating the operations on the model * Splitting the INavigationService to separate the querying from the managing functionality * Introducing specific navigation services for document, document recycle bin, media and media recycle bin * Making ContentNavigationService into a base as the functionality will be shared between the document, document recycle bin, media and media recycle bin services * Adding the implementations of document, document recycle bin, media and media recycle bin navigation services * Fixing comments * Initializing all 4 collections * Adapting the navigation unit tests to the base now * Adapting integration tests to specific navigation service * Adding test for rebuilding the structure * Adding implementation for Adding and Getting a node - needed for moving to and restoring from the recycle bin + tests * Updating the document navigation structure from the ContentService * Fix typo * Adding trashed items implementation in base - currently managing 2 structures * Removing no longer relevant GetNavigationNode and AddNavigationNode * Fix removing parent when child is removed supporting methods * Added restoring functionality * Adding Bin functionality to DocumentNavigationService * Removing Move signature from IDocumentNavigationService * Adding RecycleBin query and management services * Re-adding Move and removing GetNavigationNode and AddNavigationNode signatures from interface * Rebuilding bin structure using _documentNavigationService, instead of _documentRecycleBinNavigationService * Fixing test name * Adding more tests for remove * Adding tests for restore and removing ones for GetNavigationNode and AddNavigationNode * Remove comments * Removing document and media RecycleBinNavigationService and their interfaces * Adding media rebuild bin * Fixing initialization with correct interfaces * Removing RecycleBinNavigationServices' registration * Remove IDocumentRecycleBinNavigationService dependency * Updating in-memory nav structure when content updates happen * Adding the rest of the integration tests * Clean up IMediaNavigationService * Fix comments * Remove CustomTestSetup in integration tests as the structure is updated when content updates happen * Adding and fixing comments * Making RebuildBinAsync abstract as well * Adding DocumentNavigationServiceTestsBase * Splitting DocumentNavigationServiceTests into partial test classes * Cleaning up DocumentNavigationServiceTests since tests have been moved to specific partial classes * Reuse a method for creating content in tests * Change type in test base * Adding navigation structure updates in media service * Adding MediaNavigationServiceTestsBase * Adding integration tests for media nav str * Remove services as we will have more concrete ones * Add document and media IXNavigationQueryService and IXNavigationManagementService * Inject ManagementService in ContentService.cs and MediaService.cs * Change implementation to implement the new services + registration * Make classes sealed * Inject correct services in InitializationService * Using the right services in integration tests * Adding comments * Removing bin interfaces from main navigation ones * Rename Remove to MoveToBin * V14 QA added block list editor tests (#16862) * Added tests for blocklistEditor * Added more tets * Removed faker * Added blockTest * Updates * Added tests * Removed dependencies * Fixes * Clean up * Fixed naming * Cleaned up * Bumped version * Added missing semicolons * Added tags * Only runs the new tests * Updates * Bumped version * Fixed tests * Cleaned up * Updated version * Fixes, not done * Fixed tests * Bumped helpers * Bumped helpers * Fixed conflict * Fixed comment * Reverted to run smokeTests * Updated helpers * improve missingProperties data returned for missing propertie values (#16910) Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * update backoffice submodule * Rename initialization service to initialization hosted service * Refactor repository to return a collection * Add interface for the NavigationDto * Add constants to bind property names between DTOs * Move factory and fix input type * Use constants for column names * Use factory from base --------- Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Bjarke Berg Co-authored-by: Sven Geusens Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../Content/ContentControllerBase.cs | 19 +- .../PropertyValidationResponseModel.cs | 12 + .../DependencyInjection/UmbracoBuilder.cs | 10 +- .../Factories/NavigationFactory.cs | 45 + src/Umbraco.Core/Models/INavigationModel.cs | 24 + .../Models/Navigation/NavigationNode.cs | 30 + .../Repositories/INavigationRepository.cs | 20 + src/Umbraco.Core/Services/ContentService.cs | 175 ++- src/Umbraco.Core/Services/MediaService.cs | 119 ++- .../ContentNavigationServiceBase.cs | 344 ++++++ .../Navigation/DocumentNavigationService.cs | 18 + .../IDocumentNavigationManagementService.cs | 5 + .../IDocumentNavigationQueryService.cs | 5 + .../IMediaNavigationManagementService.cs | 5 + .../IMediaNavigationQueryService.cs | 5 + .../INavigationManagementService.cs | 57 + .../Navigation/INavigationQueryService.cs | 18 + .../IRecycleBinNavigationManagementService.cs | 40 + .../IRecycleBinNavigationQueryService.cs | 18 + .../Navigation/MediaNavigationService.cs | 18 + .../NavigationInitializationHostedService.cs | 37 + .../UmbracoBuilder.Repositories.cs | 3 +- .../Persistence/Dtos/NavigationDto.cs | 25 + .../Persistence/Dtos/NodeDto.cs | 15 +- .../Implement/ContentNavigationRepository.cs | 37 + .../UmbracoBuilderExtensions.cs | 2 + src/Umbraco.Web.UI.Client | 2 +- .../package-lock.json | 759 +------------ .../Umbraco.Tests.AcceptanceTest/package.json | 10 +- .../BlockListEditor/BlockListBlocks.spec.ts | 419 ++++++++ .../BlockListEditor/BlockListEditor.spec.ts | 311 ++++++ .../DocumentNavigationServiceTests.Copy.cs | 46 + .../DocumentNavigationServiceTests.Create.cs | 39 + .../DocumentNavigationServiceTests.Delete.cs | 43 + ...gationServiceTests.DeleteFromRecycleBin.cs | 30 + .../DocumentNavigationServiceTests.Move.cs | 39 + ...NavigationServiceTests.MoveToRecycleBin.cs | 32 + .../DocumentNavigationServiceTests.Rebuild.cs | 59 ++ .../DocumentNavigationServiceTests.Restore.cs | 50 + .../DocumentNavigationServiceTests.Update.cs | 54 + .../DocumentNavigationServiceTests.cs | 90 ++ .../DocumentNavigationServiceTestsBase.cs | 51 + .../MediaNavigationServiceTests.Create.cs | 9 + .../MediaNavigationServiceTests.Delete.cs | 9 + ...gationServiceTests.DeleteFromRecycleBin.cs | 9 + .../MediaNavigationServiceTests.Move.cs | 9 + ...NavigationServiceTests.MoveToRecycleBin.cs | 9 + .../MediaNavigationServiceTests.Rebuild.cs | 59 ++ .../MediaNavigationServiceTests.Restore.cs | 9 + .../MediaNavigationServiceTests.Update.cs | 9 + .../Services/MediaNavigationServiceTests.cs | 79 ++ .../MediaNavigationServiceTestsBase.cs | 51 + .../Umbraco.Tests.Integration.csproj | 51 + .../ContentNavigationServiceBaseTests.cs | 996 ++++++++++++++++++ 54 files changed, 3669 insertions(+), 770 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs create mode 100644 src/Umbraco.Core/Factories/NavigationFactory.cs create mode 100644 src/Umbraco.Core/Models/INavigationModel.cs create mode 100644 src/Umbraco.Core/Models/Navigation/NavigationNode.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs create mode 100644 src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs create mode 100644 src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs create mode 100644 src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts create mode 100644 tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs index a2277820bf..c008dad102 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentControllerBase.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Management.ViewModels.Content; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentEditing.Validation; using Umbraco.Cms.Core.Services.OperationStatus; @@ -9,6 +11,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content; public abstract class ContentControllerBase : ManagementApiControllerBase { + protected IActionResult ContentEditingOperationStatusResult(ContentEditingOperationStatus status) => OperationStatusResult(status, problemDetailsBuilder => status switch { @@ -96,7 +99,8 @@ public abstract class ContentControllerBase : ManagementApiControllerBase } var errors = new SortedDictionary(); - var missingPropertyAliases = new List(); + + var missingPropertyModels = new List(); foreach (PropertyValidationError validationError in validationResult.ValidationErrors) { TValueModel? requestValue = requestModel.Values.FirstOrDefault(value => @@ -105,7 +109,7 @@ public abstract class ContentControllerBase : ManagementApiControllerBase && value.Segment == validationError.Segment); if (requestValue is null) { - missingPropertyAliases.Add(validationError.Alias); + missingPropertyModels.Add(MapMissingProperty(validationError)); continue; } @@ -119,7 +123,16 @@ public abstract class ContentControllerBase : ManagementApiControllerBase .WithTitle("Validation failed") .WithDetail("One or more properties did not pass validation") .WithRequestModelErrors(errors) - .WithExtension("missingProperties", missingPropertyAliases.ToArray()) + .WithExtension("missingValues", missingPropertyModels.ToArray()) .Build())); } + + private PropertyValidationResponseModel MapMissingProperty(PropertyValidationError source) => + new() + { + Alias = source.Alias, + Segment = source.Segment, + Culture = source.Culture, + Messages = source.ErrorMessages, + }; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs new file mode 100644 index 0000000000..6f8d918c3e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/PropertyValidationResponseModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Content; + +public class PropertyValidationResponseModel +{ + public string[] Messages { get; set; } = Array.Empty(); + + public string Alias { get; set; } = string.Empty; + + public string? Culture { get; set; } + + public string? Segment { get; set; } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index ffeafe17b7..c39f05cc5e 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -39,6 +39,7 @@ using Umbraco.Cms.Core.Preview; using Umbraco.Cms.Core.Security.Authorization; using Umbraco.Cms.Core.Services.FileSystem; using Umbraco.Cms.Core.Services.ImportExport; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Services.Querying.RecycleBin; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; @@ -338,8 +339,7 @@ namespace Umbraco.Cms.Core.DependencyInjection factory.GetRequiredService(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService() - )); + factory.GetRequiredService())); Services.AddUnique(); Services.AddUnique(factory => factory.GetRequiredService()); Services.AddUnique(factory => new LocalizedTextService( @@ -352,6 +352,12 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); Services.AddUnique(); Services.AddUnique(); + Services.AddUnique(); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(); + Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(x => x.GetRequiredService()); // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs new file mode 100644 index 0000000000..316c6031d6 --- /dev/null +++ b/src/Umbraco.Core/Factories/NavigationFactory.cs @@ -0,0 +1,45 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Navigation; + +namespace Umbraco.Cms.Core.Factories; + +internal static class NavigationFactory +{ + /// + /// Builds a dictionary of NavigationNode objects from a given dataset. + /// + /// The objects used to build the navigation nodes dictionary. + /// A dictionary of objects with key corresponding to their unique Guid. + public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable entities) + { + var nodesStructure = new ConcurrentDictionary(); + var entityList = entities.ToList(); + var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); + + foreach (INavigationModel entity in entityList) + { + var node = new NavigationNode(entity.Key); + nodesStructure[entity.Key] = node; + + // We don't set the parent for items under root, it will stay null + if (entity.ParentId == -1) + { + continue; + } + + if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false) + { + continue; + } + + // If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well) + if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode)) + { + parentNode.AddChild(node); + } + } + + return nodesStructure; + } +} diff --git a/src/Umbraco.Core/Models/INavigationModel.cs b/src/Umbraco.Core/Models/INavigationModel.cs new file mode 100644 index 0000000000..bc33e22f0f --- /dev/null +++ b/src/Umbraco.Core/Models/INavigationModel.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Cms.Core.Models; + +public interface INavigationModel +{ + /// + /// Gets or sets the integer identifier of the entity. + /// + int Id { get; set; } + + /// + /// Gets or sets the Guid unique identifier of the entity. + /// + Guid Key { get; set; } + + /// + /// Gets or sets the integer identifier of the parent entity. + /// + int ParentId { get; set; } + + /// + /// Gets or sets a value indicating whether this entity is in the recycle bin. + /// + bool Trashed { get; set; } +} diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs new file mode 100644 index 0000000000..9edf00d6fb --- /dev/null +++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs @@ -0,0 +1,30 @@ +namespace Umbraco.Cms.Core.Models.Navigation; + +public sealed class NavigationNode +{ + private List _children; + + public Guid Key { get; private set; } + + public NavigationNode? Parent { get; private set; } + + public IEnumerable Children => _children.AsEnumerable(); + + public NavigationNode(Guid key) + { + Key = key; + _children = new List(); + } + + public void AddChild(NavigationNode child) + { + child.Parent = this; + _children.Add(child); + } + + public void RemoveChild(NavigationNode child) + { + _children.Remove(child); + child.Parent = null; + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs new file mode 100644 index 0000000000..cc65d637a8 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/INavigationRepository.cs @@ -0,0 +1,20 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface INavigationRepository +{ + /// + /// Retrieves a collection of content nodes as navigation models based on the object type key. + /// + /// The unique identifier for the object type. + /// A collection of navigation models. + IEnumerable GetContentNodesByObjectType(Guid objectTypeKey); + + /// + /// Retrieves a collection of trashed content nodes as navigation models based on the object type key. + /// + /// The unique identifier for the object type. + /// A collection of navigation models. + IEnumerable GetTrashedContentNodesByObjectType(Guid objectTypeKey); +} diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index dd61585203..0b629774a3 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -33,25 +34,27 @@ public class ContentService : RepositoryService, IContentService private readonly IShortStringHelper _shortStringHelper; private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private IQuery? _queryNotTrashed; #region Constructors public ContentService( - ICoreScopeProvider provider, - ILoggerFactory loggerFactory, - IEventMessagesFactory eventMessagesFactory, - IDocumentRepository documentRepository, - IEntityRepository entityRepository, - IAuditRepository auditRepository, - IContentTypeRepository contentTypeRepository, - IDocumentBlueprintRepository documentBlueprintRepository, - ILanguageRepository languageRepository, - Lazy propertyValidationService, - IShortStringHelper shortStringHelper, - ICultureImpactFactory cultureImpactFactory, - IUserIdKeyResolver userIdKeyResolver) - : base(provider, loggerFactory, eventMessagesFactory) + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver, + IDocumentNavigationManagementService documentNavigationManagementService) + : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; _entityRepository = entityRepository; @@ -63,9 +66,43 @@ public class ContentService : RepositoryService, IContentService _shortStringHelper = shortStringHelper; _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; + _documentNavigationManagementService = documentNavigationManagementService; _logger = loggerFactory.CreateLogger(); } + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] + public ContentService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDocumentRepository documentRepository, + IEntityRepository entityRepository, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IDocumentBlueprintRepository documentBlueprintRepository, + ILanguageRepository languageRepository, + Lazy propertyValidationService, + IShortStringHelper shortStringHelper, + ICultureImpactFactory cultureImpactFactory, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + loggerFactory, + eventMessagesFactory, + documentRepository, + entityRepository, + auditRepository, + contentTypeRepository, + documentBlueprintRepository, + languageRepository, + propertyValidationService, + shortStringHelper, + cultureImpactFactory, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] public ContentService( ICoreScopeProvider provider, @@ -93,7 +130,8 @@ public class ContentService : RepositoryService, IContentService propertyValidationService, shortStringHelper, cultureImpactFactory, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1034,6 +1072,11 @@ public class ContentService : RepositoryService, IContentService // have always changed if it's been saved in the back office but that's not really fail safe. _documentRepository.Save(content); + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule", + () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); + if (contentSchedule != null) { _documentRepository.PersistContentSchedule(content, contentSchedule); @@ -1097,6 +1140,11 @@ public class ContentService : RepositoryService, IContentService content.WriterId = userId; _documentRepository.Save(content); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save", + () => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key)); } scope.Notifications.Publish( @@ -2288,6 +2336,26 @@ public class ContentService : RepositoryService, IContentService } DoDelete(content); + + if (content.Trashed) + { + // Updates in-memory navigation structure for recycle bin items + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed", + () => _documentNavigationManagementService.RemoveFromBin(content.Key)); + } + else + { + // Updates in-memory navigation structure for both documents and recycle bin items + // as the item needs to be deleted whether it is in the recycle bin or not + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.DeleteLocked", + () => + { + _documentNavigationManagementService.MoveToBin(content.Key); + _documentNavigationManagementService.RemoveFromBin(content.Key); + }); + } } // TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -2512,6 +2580,8 @@ public class ContentService : RepositoryService, IContentService // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash) { + // Needed to update the in-memory navigation structure + var cameFromRecycleBin = content.ParentId == Constants.System.RecycleBinContent; content.WriterId = userId; content.ParentId = parentId; @@ -2560,6 +2630,33 @@ public class ContentService : RepositoryService, IContentService } } while (total > pageSize); + + if (parentId == Constants.System.RecycleBinContent) + { + // Updates in-memory navigation structure for both document items and recycle bin items + // as we are moving to recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin", + () => _documentNavigationManagementService.MoveToBin(content.Key)); + } + else + { + if (cameFromRecycleBin) + { + // Updates in-memory navigation structure for both document items and recycle bin items + // as we are restoring from recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore", + () => _documentNavigationManagementService.RestoreFromBin(content.Key, parent?.Key)); + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked", + () => _documentNavigationManagementService.Move(content.Key, parent?.Key)); + } + } } private void PerformMoveContentLocked(IContent content, int userId, bool? trash) @@ -2663,6 +2760,9 @@ public class ContentService : RepositoryService, IContentService { EventMessages eventMessages = EventMessagesFactory.Get(); + // keep track of updates (copied item key and parent key) for the in-memory navigation structure + var navigationUpdates = new List>(); + IContent copy = content.DeepCloneWithResetIdentities(); copy.ParentId = parentId; @@ -2699,6 +2799,9 @@ public class ContentService : RepositoryService, IContentService // save and flush because we need the ID for the recursive Copying events _documentRepository.Save(copy); + // store navigation update information for copied item + navigationUpdates.Add(Tuple.Create(copy.Key, GetParent(copy)?.Key)); + // add permissions if (currentPermissions.Count > 0) { @@ -2750,12 +2853,29 @@ public class ContentService : RepositoryService, IContentService // save and flush (see above) _documentRepository.Save(descendantCopy); + // store navigation update information for descendants + navigationUpdates.Add(Tuple.Create(descendantCopy.Key, GetParent(descendantCopy)?.Key)); + copies.Add(Tuple.Create(descendant, descendantCopy)); idmap[descendant.Id] = descendantCopy.Id; } } } + if (navigationUpdates.Count > 0) + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Copy", + () => + { + foreach (Tuple update in navigationUpdates) + { + _documentNavigationManagementService.Add(update.Item1, update.Item2); + } + }); + } + // not handling tags here, because // - tags should be handled by the content repository // - a copy is unpublished and therefore has no impact on tags in DB @@ -3697,4 +3817,29 @@ public class ContentService : RepositoryService, IContentService DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId); #endregion + + /// + /// Enlists an action in the current scope context to update the in-memory navigation structure + /// when the scope completes successfully. + /// + /// The unique key identifying the action to be enlisted. + /// The action to be performed for updating the in-memory navigation structure. + /// Thrown when the scope context is null and therefore cannot be used. + private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) + { + IScopeContext? scopeContext = ScopeProvider.Context; + + if (scopeContext is null) + { + throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); + } + + scopeContext.Enlist(enlistingActionKey, completed => + { + if (completed) + { + updateNavigation(); + } + }); + } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index c754aa244a..ae334447e9 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Strings; using Umbraco.Extensions; @@ -28,6 +29,7 @@ namespace Umbraco.Cms.Core.Services private readonly IEntityRepository _entityRepository; private readonly IShortStringHelper _shortStringHelper; private readonly IUserIdKeyResolver _userIdKeyResolver; + private readonly IMediaNavigationManagementService _mediaNavigationManagementService; private readonly MediaFileManager _mediaFileManager; @@ -43,7 +45,8 @@ namespace Umbraco.Cms.Core.Services IMediaTypeRepository mediaTypeRepository, IEntityRepository entityRepository, IShortStringHelper shortStringHelper, - IUserIdKeyResolver userIdKeyResolver) + IUserIdKeyResolver userIdKeyResolver, + IMediaNavigationManagementService mediaNavigationManagementService) : base(provider, loggerFactory, eventMessagesFactory) { _mediaFileManager = mediaFileManager; @@ -53,6 +56,34 @@ namespace Umbraco.Cms.Core.Services _entityRepository = entityRepository; _shortStringHelper = shortStringHelper; _userIdKeyResolver = userIdKeyResolver; + _mediaNavigationManagementService = mediaNavigationManagementService; + } + + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] + public MediaService( + ICoreScopeProvider provider, + MediaFileManager mediaFileManager, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IMediaRepository mediaRepository, + IAuditRepository auditRepository, + IMediaTypeRepository mediaTypeRepository, + IEntityRepository entityRepository, + IShortStringHelper shortStringHelper, + IUserIdKeyResolver userIdKeyResolver) + : this( + provider, + mediaFileManager, + loggerFactory, + eventMessagesFactory, + mediaRepository, + auditRepository, + mediaTypeRepository, + entityRepository, + shortStringHelper, + userIdKeyResolver, + StaticServiceProvider.Instance.GetRequiredService()) + { } [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] @@ -76,8 +107,8 @@ namespace Umbraco.Cms.Core.Services mediaTypeRepository, entityRepository, shortStringHelper, - StaticServiceProvider.Instance.GetRequiredService() - ) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -769,6 +800,12 @@ namespace Umbraco.Cms.Core.Services media.WriterId = userId; _mediaRepository.Save(media); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.Save", + () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); + scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification)); // TODO: See note about suppressing events in content service scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages)); @@ -810,6 +847,11 @@ namespace Umbraco.Cms.Core.Services } _mediaRepository.Save(media); + + // Updates in-memory navigation structure - we only handle new items, other updates are not a concern + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.ContentService.Save-collection", + () => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key)); } scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification)); @@ -881,6 +923,26 @@ namespace Umbraco.Cms.Core.Services } DoDelete(media); + + if (media.Trashed) + { + // Updates in-memory navigation structure for recycle bin items + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed", + () => _mediaNavigationManagementService.RemoveFromBin(media.Key)); + } + else + { + // Updates in-memory navigation structure for both media and recycle bin items + // as the item needs to be deleted whether it is in the recycle bin or not + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.DeleteLocked", + () => + { + _mediaNavigationManagementService.MoveToBin(media.Key); + _mediaNavigationManagementService.RemoveFromBin(media.Key); + }); + } } //TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way @@ -1069,6 +1131,8 @@ namespace Umbraco.Cms.Core.Services // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { + // Needed to update the in-memory navigation structure + var cameFromRecycleBin = media.ParentId == Constants.System.RecycleBinMedia; media.ParentId = parentId; // get the level delta (old pos to new pos) @@ -1114,6 +1178,32 @@ namespace Umbraco.Cms.Core.Services } while (total > pageSize); + if (parentId == Constants.System.RecycleBinMedia) + { + // Updates in-memory navigation structure for both media items and recycle bin items + // as we are moving to recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin", + () => _mediaNavigationManagementService.MoveToBin(media.Key)); + } + else + { + if (cameFromRecycleBin) + { + // Updates in-memory navigation structure for both media items and recycle bin items + // as we are restoring from recycle bin + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore", + () => _mediaNavigationManagementService.RestoreFromBin(media.Key, parent?.Key)); + } + else + { + // Updates in-memory navigation structure + UpdateInMemoryNavigationStructure( + "Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked", + () => _mediaNavigationManagementService.Move(media.Key, parent?.Key)); + } + } } private void PerformMoveMediaLocked(IMedia media, bool? trash) @@ -1421,6 +1511,29 @@ namespace Umbraco.Cms.Core.Services #endregion + /// + /// Enlists an action in the current scope context to update the in-memory navigation structure + /// when the scope completes successfully. + /// + /// The unique key identifying the action to be enlisted. + /// The action to be performed for updating the in-memory navigation structure. + /// Thrown when the scope context is null and therefore cannot be used. + private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation) + { + IScopeContext? scopeContext = ScopeProvider.Context; + if (scopeContext is null) + { + throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used."); + } + + scopeContext.Enlist(enlistingActionKey, completed => + { + if (completed) + { + updateNavigation(); + } + }); + } } } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs new file mode 100644 index 0000000000..e5755c8d87 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -0,0 +1,344 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core.Factories; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Navigation; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal abstract class ContentNavigationServiceBase +{ + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly INavigationRepository _navigationRepository; + private ConcurrentDictionary _navigationStructure = new(); + private ConcurrentDictionary _recycleBinNavigationStructure = new(); + + protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + { + _coreScopeProvider = coreScopeProvider; + _navigationRepository = navigationRepository; + } + + /// + /// Rebuilds the entire main navigation structure. Implementations should define how the structure is rebuilt. + /// + public abstract Task RebuildAsync(); + + /// + /// Rebuilds the recycle bin navigation structure. Implementations should define how the bin structure is rebuilt. + /// + public abstract Task RebuildBinAsync(); + + public bool TryGetParentKey(Guid childKey, out Guid? parentKey) + => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); + + public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) + => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); + + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) + => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); + + public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys) + => TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys); + + public bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys) + => TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys); + + public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey) + => TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey); + + public bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable childrenKeys) + => TryGetChildrenKeysFromStructure(_recycleBinNavigationStructure, parentKey, out childrenKeys); + + public bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable descendantsKeys) + => TryGetDescendantsKeysFromStructure(_recycleBinNavigationStructure, parentKey, out descendantsKeys); + + public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancestorsKeys) + => TryGetAncestorsKeysFromStructure(_recycleBinNavigationStructure, childKey, out ancestorsKeys); + + public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys) + => TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys); + + public bool MoveToBin(Guid key) + { + if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) + { + return false; // Node doesn't exist + } + + // Recursively remove all descendants and add them to recycle bin + AddDescendantsToRecycleBinRecursively(nodeToRemove); + + return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) && + _navigationStructure.TryRemove(key, out _); + } + + public bool Add(Guid key, Guid? parentKey = null) + { + NavigationNode? parentNode = null; + if (parentKey.HasValue) + { + if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false) + { + return false; // Parent node doesn't exist + } + } + + var newNode = new NavigationNode(key); + if (_navigationStructure.TryAdd(key, newNode) is false) + { + return false; // Node with this key already exists + } + + parentNode?.AddChild(newNode); + + return true; + } + + public bool Move(Guid key, Guid? targetParentKey = null) + { + if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToMove) is false) + { + return false; // Node doesn't exist + } + + if (key == targetParentKey) + { + return false; // Cannot move a node to itself + } + + NavigationNode? targetParentNode = null; + if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + + // Remove the node from its current parent's children list + if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode)) + { + currentParentNode.RemoveChild(nodeToMove); + } + + // Set the new parent for the node (if parent node is null - the node is moved to root) + targetParentNode?.AddChild(nodeToMove); + + return true; + } + + public bool RemoveFromBin(Guid key) + { + if (TryRemoveNodeFromParentInStructure(_recycleBinNavigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null) + { + return false; // Node doesn't exist + } + + RemoveDescendantsRecursively(nodeToRemove); + + return _recycleBinNavigationStructure.TryRemove(key, out _); + } + + public bool RestoreFromBin(Guid key, Guid? targetParentKey = null) + { + if (_recycleBinNavigationStructure.TryGetValue(key, out NavigationNode? nodeToRestore) is false) + { + return false; // Node doesn't exist + } + + // If a target parent is specified, try to find it in the main structure + NavigationNode? targetParentNode = null; + if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false) + { + return false; // Target parent doesn't exist + } + + // Set the new parent for the node (if parent node is null - the node is moved to root) + targetParentNode?.AddChild(nodeToRestore); + + // Restore the node and its descendants from the recycle bin to the main structure + RestoreNodeAndDescendantsRecursively(nodeToRestore); + + return _navigationStructure.TryAdd(nodeToRestore.Key, nodeToRestore) && + _recycleBinNavigationStructure.TryRemove(key, out _); + } + + /// + /// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed. + /// Only relevant for items in the content and media trees (which have readLock values of -333 or -334). + /// + /// The read lock value, should be -333 or -334 for content and media trees. + /// The key of the object type to rebuild. + /// Indicates whether the items are in the recycle bin. + protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool trashed) + { + // This is only relevant for items in the content and media trees + if (readLock != Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree) + { + return; + } + + using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); + scope.ReadLock(readLock); + + IEnumerable navigationModels = trashed ? + _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) : + _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + + _navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels); + } + + private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey) + { + if (structure.TryGetValue(childKey, out NavigationNode? childNode)) + { + parentKey = childNode.Parent?.Key; + return true; + } + + // Child doesn't exist + parentKey = null; + return false; + } + + private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable childrenKeys) + { + if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) + { + // Parent doesn't exist + childrenKeys = []; + return false; + } + + childrenKeys = parentNode.Children.Select(child => child.Key); + return true; + } + + private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) + { + var descendants = new List(); + + if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) + { + // Parent doesn't exist + descendantsKeys = []; + return false; + } + + GetDescendantsRecursively(parentNode, descendants); + + descendantsKeys = descendants; + return true; + } + + private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary structure, Guid childKey, out IEnumerable ancestorsKeys) + { + var ancestors = new List(); + + if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false) + { + // Child doesn't exist + ancestorsKeys = []; + return false; + } + + while (childNode?.Parent is not null) + { + ancestors.Add(childNode.Parent.Key); + childNode = childNode.Parent; + } + + ancestorsKeys = ancestors; + return true; + } + + private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary structure, Guid key, out IEnumerable siblingsKeys) + { + siblingsKeys = []; + + if (structure.TryGetValue(key, out NavigationNode? node) is false) + { + return false; // Node doesn't exist + } + + if (node.Parent is null) + { + // To find siblings of a node at root level, we need to iterate over all items and add those with null Parent + siblingsKeys = structure + .Where(kv => kv.Value.Parent is null && kv.Key != key) + .Select(kv => kv.Key) + .ToList(); + return true; + } + + if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false) + { + return false; // Couldn't retrieve children keys + } + + // Filter out the node itself to get its siblings + siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList(); + return true; + } + + private void GetDescendantsRecursively(NavigationNode node, List descendants) + { + foreach (NavigationNode child in node.Children) + { + descendants.Add(child.Key); + GetDescendantsRecursively(child, descendants); + } + } + + private bool TryRemoveNodeFromParentInStructure(ConcurrentDictionary structure, Guid key, out NavigationNode? nodeToRemove) + { + if (structure.TryGetValue(key, out nodeToRemove) is false) + { + return false; // Node doesn't exist + } + + // Remove the node from its parent's children list + if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode)) + { + parentNode.RemoveChild(nodeToRemove); + } + + return true; + } + + private void AddDescendantsToRecycleBinRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + AddDescendantsToRecycleBinRecursively(child); + + // Only remove the child from the main structure if it was successfully added to the recycle bin + if (_recycleBinNavigationStructure.TryAdd(child.Key, child)) + { + _navigationStructure.TryRemove(child.Key, out _); + } + } + } + + private void RemoveDescendantsRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + RemoveDescendantsRecursively(child); + _recycleBinNavigationStructure.TryRemove(child.Key, out _); + } + } + + private void RestoreNodeAndDescendantsRecursively(NavigationNode node) + { + foreach (NavigationNode child in node.Children) + { + RestoreNodeAndDescendantsRecursively(child); + + // Only remove the child from the recycle bin structure if it was successfully added to the main one + if (_navigationStructure.TryAdd(child.Key, child)) + { + _recycleBinNavigationStructure.TryRemove(child.Key, out _); + } + } + } +} diff --git a/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs new file mode 100644 index 0000000000..44804d07c6 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/DocumentNavigationService.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService +{ + public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false); + + public override async Task RebuildBinAsync() + => await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true); +} diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs new file mode 100644 index 0000000000..789256329e --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationManagementService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs new file mode 100644 index 0000000000..d8fb5e0c29 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IDocumentNavigationQueryService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IDocumentNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs new file mode 100644 index 0000000000..95cb1c9616 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationManagementService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs new file mode 100644 index 0000000000..e3f46f4211 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IMediaNavigationQueryService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IMediaNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService +{ +} diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs new file mode 100644 index 0000000000..4ab8458f18 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs @@ -0,0 +1,57 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for managing the navigation structure. +/// +public interface INavigationManagementService +{ + /// + /// Rebuilds the entire navigation structure by refreshing the navigation tree based + /// on the current state of the underlying repository. + /// + Task RebuildAsync(); + + /// + /// Removes a node from the main navigation structure and moves it, along with + /// its descendants, to the root of the recycle bin structure. + /// + /// The unique identifier of the node to remove. + /// + /// true if the node and its descendants were successfully removed from the + /// main navigation structure and added to the recycle bin; otherwise, false. + /// + bool MoveToBin(Guid key); + + /// + /// Adds a new node to the main navigation structure. If a parent key is provided, + /// the new node is added as a child of the specified parent. If no parent key is + /// provided, the new node is added at the root level. + /// + /// The unique identifier of the new node to add. + /// + /// The unique identifier of the parent node. If null, the new node will be added to + /// the root level. + /// + /// + /// true if the node was successfully added to the main navigation structure; + /// otherwise, false. + /// + bool Add(Guid key, Guid? parentKey = null); + + /// + /// Moves an existing node to a new parent in the main navigation structure. If a + /// target parent key is provided, the node is moved under the specified parent. + /// If no target parent key is provided, the node is moved to the root level. + /// + /// The unique identifier of the node to move. + /// + /// The unique identifier of the new parent node. If null, the node will be moved to + /// the root level. + /// + /// + /// true if the node and its descendants were successfully moved to the new parent + /// in the main navigation structure; otherwise, false. + /// + bool Move(Guid key, Guid? targetParentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs new file mode 100644 index 0000000000..4e28f80bb6 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for querying the navigation structure. +/// +public interface INavigationQueryService +{ + bool TryGetParentKey(Guid childKey, out Guid? parentKey); + + bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); + + bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); + + bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys); + + bool TryGetSiblingsKeys(Guid key, out IEnumerable siblingsKeys); +} diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs new file mode 100644 index 0000000000..07c4151362 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationManagementService.cs @@ -0,0 +1,40 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for managing the recycle bin navigation structure. +/// +public interface IRecycleBinNavigationManagementService +{ + /// + /// Rebuilds the recycle bin navigation structure by fetching the latest trashed nodes + /// from the underlying repository. + /// + Task RebuildBinAsync(); + + /// + /// Permanently removes a node and all of its descendants from the recycle bin navigation structure. + /// + /// The unique identifier of the node to remove. + /// + /// true if the node and its descendants were successfully removed from the recycle bin; + /// otherwise, false. + /// + bool RemoveFromBin(Guid key); + + /// + /// Restores a node and all of its descendants from the recycle bin navigation structure and moves them back + /// to the main navigation structure. The node can be restored to a specified target parent or to the root + /// level if no parent is specified. + /// + /// The unique identifier of the node to restore from the recycle bin navigation structure. + /// + /// The unique identifier of the target parent node in the main navigation structure to which the node + /// should be restored. If null, the node will be restored to the root level. + /// + /// + /// true if the node and its descendants were successfully restored to the main navigation structure; + /// otherwise, false. + /// + bool RestoreFromBin(Guid key, Guid? targetParentKey = null); +} diff --git a/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs new file mode 100644 index 0000000000..0a57f5346c --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/IRecycleBinNavigationQueryService.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Placeholder for sharing logic between the document and media navigation services +/// for querying the recycle bin navigation structure. +/// +public interface IRecycleBinNavigationQueryService +{ + bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey); + + bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable childrenKeys); + + bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable descendantsKeys); + + bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable ancestorsKeys); + + bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable siblingsKeys); +} diff --git a/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs new file mode 100644 index 0000000000..62ab5a1617 --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/MediaNavigationService.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService +{ + public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + public override async Task RebuildAsync() + => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false); + + public override async Task RebuildBinAsync() + => await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true); +} diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs new file mode 100644 index 0000000000..274158d40d --- /dev/null +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs @@ -0,0 +1,37 @@ +using Microsoft.Extensions.Hosting; + +namespace Umbraco.Cms.Core.Services.Navigation; + +/// +/// Responsible for seeding the in-memory navigation structures at application's startup +/// by rebuild the navigation structures. +/// +public sealed class NavigationInitializationHostedService : IHostedLifecycleService +{ + private readonly IDocumentNavigationManagementService _documentNavigationManagementService; + private readonly IMediaNavigationManagementService _mediaNavigationManagementService; + + public NavigationInitializationHostedService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService) + { + _documentNavigationManagementService = documentNavigationManagementService; + _mediaNavigationManagementService = mediaNavigationManagementService; + } + + public async Task StartingAsync(CancellationToken cancellationToken) + { + await _documentNavigationManagementService.RebuildAsync(); + await _documentNavigationManagementService.RebuildBinAsync(); + await _mediaNavigationManagementService.RebuildAsync(); + await _mediaNavigationManagementService.RebuildBinAsync(); + } + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + + public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index f62227ddba..757e103727 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.DynamicRoot.QuerySteps; using Umbraco.Cms.Infrastructure.Persistence.Repositories; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Services.Implement; @@ -79,6 +79,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs new file mode 100644 index 0000000000..156a85b19c --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs @@ -0,0 +1,25 @@ +using NPoco; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Persistence.Dtos; + +// Used internally for representing the data needed for constructing the in-memory navigation structure. +[TableName(NodeDto.TableName)] +internal class NavigationDto : INavigationModel +{ + /// + [Column(NodeDto.IdColumnName)] + public int Id { get; set; } + + /// + [Column(NodeDto.KeyColumnName)] + public Guid Key { get; set; } + + /// + [Column(NodeDto.ParentIdColumnName)] + public int ParentId { get; set; } + + /// + [Column(NodeDto.TrashedColumnName)] + public bool Trashed { get; set; } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs index 5bf3a26207..2ac62429ba 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs @@ -12,19 +12,26 @@ public class NodeDto { public const string TableName = Constants.DatabaseSchema.Tables.Node; public const int NodeIdSeed = 1060; + + // Public constants to bind properties between DTOs + public const string IdColumnName = "id"; + public const string KeyColumnName = "uniqueId"; + public const string ParentIdColumnName = "parentId"; + public const string TrashedColumnName = "trashed"; + private int? _userId; - [Column("id")] + [Column(IdColumnName)] [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] public int NodeId { get; set; } - [Column("uniqueId")] + [Column(KeyColumnName)] [NullSetting(NullSetting = NullSettings.NotNull)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")] [Constraint(Default = SystemMethods.NewGuid)] public Guid UniqueId { get; set; } - [Column("parentId")] + [Column(ParentIdColumnName)] [ForeignKey(typeof(NodeDto))] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_parentId_nodeObjectType", ForColumns = "parentID,nodeObjectType", IncludeColumns = "trashed,nodeUser,level,path,sortOrder,uniqueID,text,createDate")] public int ParentId { get; set; } @@ -43,7 +50,7 @@ public class NodeDto [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType_trashed_sorted", ForColumns = "nodeObjectType,trashed,sortOrder,id", IncludeColumns = "uniqueID,parentID,level,path,nodeUser,text,createDate")] public int SortOrder { get; set; } - [Column("trashed")] + [Column(TrashedColumnName)] [Constraint(Default = "0")] [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")] public bool Trashed { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs new file mode 100644 index 0000000000..2f86d00143 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentNavigationRepository.cs @@ -0,0 +1,37 @@ +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class ContentNavigationRepository : INavigationRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public ContentNavigationRepository(IScopeAccessor scopeAccessor) + => _scopeAccessor = scopeAccessor; + + private IScope? AmbientScope => _scopeAccessor.AmbientScope; + + /// + public IEnumerable GetContentNodesByObjectType(Guid objectTypeKey) + => FetchNavigationDtos(objectTypeKey, false); + + /// + public IEnumerable GetTrashedContentNodesByObjectType(Guid objectTypeKey) + => FetchNavigationDtos(objectTypeKey, true); + + private IEnumerable FetchNavigationDtos(Guid objectTypeKey, bool trashed) + { + Sql? sql = AmbientScope?.SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == objectTypeKey && x.Trashed == trashed) + .OrderBy(x => x.Path); // make sure that we get the parent items first + + return AmbientScope?.Database.Fetch(sql) ?? Enumerable.Empty(); + } +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 85a9d7ded4..ea404d9703 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -30,6 +30,7 @@ using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Preview; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.BackgroundJobs; @@ -193,6 +194,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory); builder.Services.AddHostedService(); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); return builder; } diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index a9d3a43969..a2fc54b77e 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit a9d3a4396968e4cc47c1d1cd290ca8b1cf764e12 +Subproject commit a2fc54b77e99de28a0669ab628ecfd7983df7ad8 diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index f7103ba5a4..d3d88edf60 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -11,20 +11,14 @@ "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" }, "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" } }, "node_modules/@colors/colors": { @@ -36,96 +30,25 @@ "node": ">=0.1.90" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "dev": true - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@playwright/test": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", - "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "dependencies": { - "playwright": "1.43.1" + "playwright": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/@sideway/address": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", - "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "dev": true - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true - }, "node_modules/@types/node": { - "version": "20.10.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", - "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -148,78 +71,12 @@ "node-fetch": "^2.6.7" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "dev": true }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", - "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -228,15 +85,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", @@ -246,23 +94,6 @@ "node": ">=0.1.90" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -272,57 +103,15 @@ "node": ">=0.4.0" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dotenv": { - "version": "16.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", - "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" + "url": "https://dotenvx.com" } }, "node_modules/eyes": { @@ -334,329 +123,24 @@ "node": "> 0.1.90" } }, - "node_modules/faker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz", - "integrity": "sha512-ILKg69P6y/D8/wSmDXw35Ly0re8QzQ8pMfBCflsGiZG2ZjMUNLYNexA6lz5pkmJlepVdsiDFUxYAzPQ9/+iGLA==" - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", - "integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "dev": true }, - "node_modules/joi": { - "version": "17.11.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", - "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", - "dev": true, - "dependencies": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.3", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", "dev": true }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "bin": { - "ncp": "bin/ncp" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -676,88 +160,34 @@ } } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/playwright": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", - "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "dependencies": { - "playwright-core": "1.43.1" + "playwright-core": "1.46.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.43.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", - "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/prompt": { @@ -776,32 +206,6 @@ "node": ">= 6.0.0" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -814,16 +218,6 @@ "node": ">=0.8" } }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", @@ -833,62 +227,6 @@ "node": ">= 0.4.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -898,27 +236,15 @@ "node": "*" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true }, "node_modules/typescript": { @@ -940,25 +266,6 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/wait-on": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", - "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==", - "dev": true, - "dependencies": { - "axios": "^1.6.1", - "joi": "^17.11.0", - "lodash": "^4.17.21", - "minimist": "^1.2.8", - "rxjs": "^7.8.1" - }, - "bin": { - "wait-on": "bin/wait-on" - }, - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -998,20 +305,6 @@ "dependencies": { "lodash": "^4.17.14" } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/xhr2": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", - "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", - "engines": { - "node": ">= 6" - } } } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index d6877d5581..170bdac491 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -13,21 +13,15 @@ "devDependencies": { "@playwright/test": "^1.43", "@types/node": "^20.9.0", - "del": "^6.0.0", - "ncp": "^2.0.0", "prompt": "^1.2.0", "tslib": "^2.4.0", - "typescript": "^4.8.3", - "wait-on": "^7.2.0" + "typescript": "^4.8.3" }, "dependencies": { "@umbraco/json-models-builders": "^2.0.17", "@umbraco/playwright-testhelpers": "^2.0.0-beta.78", "camelize": "^1.0.0", "dotenv": "^16.3.1", - "faker": "^4.1.0", - "form-data": "^4.0.0", - "node-fetch": "^2.6.7", - "xhr2": "^0.2.1" + "node-fetch": "^2.6.7" } } diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts new file mode 100644 index 0000000000..737cc34e9f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListBlocks.spec.ts @@ -0,0 +1,419 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can add a label to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(labelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); +}); + +test('can update a label for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const newLabelText = 'ThisIsANewLabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(newLabelText); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, newLabelText)).toBeTruthy(); +}); + +test('can remove a label from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const labelText = 'ThisIsALabel'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, labelText); + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, labelText)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.enterBlockLabelText(""); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + expect(await umbracoApi.dataType.doesBlockEditorBlockContainLabel(blockListEditorName, elementTypeId, "")).toBeTruthy(); +}); + +test('can update overlay size for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const overlaySize = 'medium'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithEditorAppearance(blockListEditorName, elementTypeId, ""); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.updateBlockOverlaySize(overlaySize); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].editorSize).toEqual(overlaySize); +}); + +test('can open content model in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.openBlockContentModel(); + + // Assert + await umbracoUi.dataType.isElementWorkspaceOpenInBlock(elementTypeName); +}); + +// TODO: Is this an issue? should you be able to remove the contentModel so you have none? +// There is currently frontend issues +test.skip('can remove a content model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockContentModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); +}); + +test('can add a settings model to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.addBlockSettingsModel(secondElementName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); +}); + +test('can remove a settings model from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementName = 'SecondElementTest'; + const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithContentAndSettingsElementType(blockListEditorName, contentElementTypeId, settingsElementTypeId); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.removeBlockSettingsModel(); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithSettingsTypeIds(blockListEditorName, [settingsElementTypeId])).toBeFalsy(); +}); + +test('can add a background color to a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(backgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); +}); + +test('can update a background color for a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const newBackgroundColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(newBackgroundColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(newBackgroundColor); +}); + +test('can delete a background color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const backgroundColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, backgroundColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(backgroundColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockBackgroundColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].backgroundColor).toEqual(''); +}); + +test('can add a icon color to a block', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(iconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); +}); + +test('can update a icon color for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const newIconColor = '#ff4444'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, "", iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(newIconColor); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(newIconColor); +}); + +test('can delete a icon color from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const iconColor = '#ff0000'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', iconColor); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(iconColor); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.selectBlockIconColor(''); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].iconColor).toEqual(''); +}); + +// TODO: Currently it is not possible to update a stylesheet to a block +test.skip('can update a custom stylesheet for a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + const secondStylesheetName = 'SecondStylesheet.css'; + const secondStylesheetPath = '/wwwroot/css/' + secondStylesheetName; + const encodedSecondStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(secondStylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(secondStylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + // Removes first stylesheet + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedSecondStylesheetPath); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.ensureNameNotExists(secondStylesheetName); +}); + +// TODO: Currently it is not possible to delete a stylesheet to a block +test.skip('can delete a custom stylesheet from a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const stylesheetName = 'TestStylesheet.css'; + const stylesheetPath = '/wwwroot/css/' + stylesheetName; + const encodedStylesheetPath = await umbracoApi.stylesheet.encodeStylesheetPath(stylesheetPath); + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); + await umbracoApi.stylesheet.createDefaultStylesheet(stylesheetName); + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithCatalogueAppearance(blockListEditorName, contentElementTypeId, '', '', encodedStylesheetPath); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toEqual(encodedStylesheetPath); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickRemoveCustomStylesheetWithName(stylesheetName); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].stylesheet[0]).toBeUndefined(); + + // Clean + await umbracoApi.stylesheet.ensureNameNotExists(stylesheetName); +}); + +test('can enable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, contentElementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); +}); + +test('can disable hide content editor in a block', async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const contentElementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListWithBlockWithHideContentEditor(blockListEditorName, contentElementTypeId, true); + let blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.goToBlockWithName(elementTypeName); + await umbracoUi.dataType.clickBlockListHideContentEditorButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + blockData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(blockData.values[0].value[0].forceHideContentEditorInOverlay).toEqual(false); +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can add a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); + +// TODO: Thumbnails are not showing in the UI +test.skip('can remove a thumbnail to a block ', {tag: '@smoke'}, async ({page, umbracoApi, umbracoUi}) => { + +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts new file mode 100644 index 0000000000..24142bca82 --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataType/BlockListEditor/BlockListEditor.spec.ts @@ -0,0 +1,311 @@ +import {test} from '@umbraco/playwright-testhelpers'; +import {expect} from "@playwright/test"; + +const blockListEditorName = 'TestBlockListEditor'; +const elementTypeName = 'BlockListElement'; +const dataTypeName = 'Textstring'; +const groupName = 'testGroup'; + +test.beforeEach(async ({umbracoUi, umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); + await umbracoUi.goToBackOffice(); + await umbracoUi.dataType.goToSettingsTreeItem('Data Types'); +}); + +test.afterEach(async ({umbracoApi}) => { + await umbracoApi.dataType.ensureNameNotExists(blockListEditorName); +}); + +test('can create a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListLocatorName = 'Block List'; + const blockListEditorAlias = 'Umbraco.BlockList'; + const blockListEditorUiAlias = 'Umb.PropertyEditorUi.BlockList'; + + // Act + await umbracoUi.dataType.clickActionsMenuAtRoot(); + await umbracoUi.dataType.clickCreateButton(); + await umbracoUi.dataType.clickNewDataTypeThreeDotsButton(); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSelectAPropertyEditorButton(); + await umbracoUi.dataType.selectAPropertyEditor(blockListLocatorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.editorAlias).toBe(blockListEditorAlias); + expect(dataTypeData.editorUiAlias).toBe(blockListEditorUiAlias); +}); + +test('can rename a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const wrongName = 'BlockGridEditorTest'; + await umbracoApi.dataType.createEmptyBlockListDataType(wrongName); + + // Act + await umbracoUi.dataType.goToDataType(wrongName); + await umbracoUi.dataType.enterDataTypeName(blockListEditorName); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesNameExist(blockListEditorName)).toBeTruthy(); + expect(await umbracoApi.dataType.doesNameExist(wrongName)).toBeFalsy(); +}); + +test('can delete a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const blockListId = await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.clickRootFolderCaretButton(); + await umbracoUi.dataType.clickActionsMenuForDataType(blockListEditorName); + await umbracoUi.dataType.clickDeleteExactButton(); + await umbracoUi.dataType.clickConfirmToDeleteButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesExist(blockListId)).toBeFalsy(); + await umbracoUi.dataType.isTreeItemVisible(blockListEditorName, false); +}); + +test('can add a block to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, 'testGroup', dataTypeName, textStringData.id); + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(elementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add multiple blocks to a block list editor', async ({umbracoApi, umbracoUi}) => { + // Arrange + const secondElementTypeName = 'SecondBlockListElement'; + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + const secondElementTypeId = await umbracoApi.documentType.createDefaultElementType(secondElementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickAddBlockButton(); + await umbracoUi.dataType.clickLabelWithName(secondElementTypeName); + await umbracoUi.dataType.clickChooseButton(); + await umbracoUi.dataType.clickSubmitButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId, secondElementTypeId])).toBeTruthy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); + await umbracoApi.documentType.ensureNameNotExists(secondElementTypeName); +}); + +test('can remove a block from a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const textStringData = await umbracoApi.dataType.getByName(dataTypeName); + const elementTypeId = await umbracoApi.documentType.createDefaultElementType(elementTypeName, groupName, dataTypeName, textStringData.id); + await umbracoApi.dataType.createBlockListDataTypeWithABlock(blockListEditorName, elementTypeId); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickRemoveBlockWithName(elementTypeName); + await umbracoUi.dataType.clickConfirmRemoveButton(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesBlockEditorContainBlocksWithContentTypeIds(blockListEditorName, [elementTypeId])).toBeFalsy(); + + // Clean + await umbracoApi.documentType.ensureNameNotExists(elementTypeName); +}); + +test('can add a min and max amount to a block list editor', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 1; + const maxAmount = 2; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMinAmount(minAmount.toString()); + await umbracoUi.dataType.enterMaxAmount(maxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + expect(dataTypeData.values[0].value.max).toBe(maxAmount); +}); + +test('max can not be less than min', async ({umbracoApi, umbracoUi}) => { + // Arrange + const minAmount = 2; + const oldMaxAmount = 2; + const newMaxAmount = 1; + await umbracoApi.dataType.createBlockListDataTypeWithMinAndMaxAmount(blockListEditorName, minAmount, oldMaxAmount); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterMaxAmount(newMaxAmount.toString()); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(false); + const dataTypeData = await umbracoApi.dataType.getByName(blockListEditorName); + await umbracoUi.dataType.doesAmountContainErrorMessageWithText('The low value must not be exceed the high value'); + expect(dataTypeData.values[0].value.min).toBe(minAmount); + // The max value should not be updated + expect(dataTypeData.values[0].value.max).toBe(oldMaxAmount); +}); + +test('can enable single block mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable single block mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithSingleBlockMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickSingleBlockMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isSingleBlockModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable live editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable live editing mode', async ({umbracoApi, umbracoUi}) => { +// Arrange + await umbracoApi.dataType.createBlockListDataTypeWithLiveEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickLiveEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isLiveEditingModeEnabledForBlockEditor(blockListEditorName, false)).toBeTruthy(); +}); + +test('can enable inline editing mode', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, false); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, true)).toBeTruthy(); +}); + +test('can disable inline editing mode', async ({umbracoApi, umbracoUi}) => { + // Arrange + await umbracoApi.dataType.createBlockListDataTypeWithInlineEditingMode(blockListEditorName, true); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.clickInlineEditingMode(); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.isInlineEditingModeEnabledForBlockList(blockListEditorName, false)).toBeTruthy(); +}); + +test('can add a property editor width', {tag: '@smoke'}, async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createEmptyBlockListDataType(blockListEditorName); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(propertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); +}); + +test('can update a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const oldPropertyWidth = '50%'; + const newPropertyWidth = '100%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, oldPropertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, oldPropertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(newPropertyWidth); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, newPropertyWidth)).toBeTruthy(); +}); + +test('can remove a property editor width', async ({umbracoApi, umbracoUi}) => { + // Arrange + const propertyWidth = '50%'; + await umbracoApi.dataType.createBlockListDataTypeWithPropertyEditorWidth(blockListEditorName, propertyWidth); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, propertyWidth)).toBeTruthy(); + + // Act + await umbracoUi.dataType.goToDataType(blockListEditorName); + await umbracoUi.dataType.enterPropertyEditorWidth(''); + await umbracoUi.dataType.clickSaveButton(); + + // Assert + await umbracoUi.dataType.isSuccessNotificationVisible(); + expect(await umbracoApi.dataType.doesMaxPropertyContainWidthForBlockEditor(blockListEditorName, '')).toBeTruthy(); +}); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs new file mode 100644 index 0000000000..b3ebfa2215 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs @@ -0,0 +1,46 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: test that it is added to its new parent - check parent's children +// TODO: test that it has the same amount of descendants - depending on value of includeDescendants param +// TODO: test that the number of target parent descendants updates when copying node with descendants +// TODO: test that copied node descendants have different keys than source node descendants +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 to itself + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", null)] // Child 2 to content root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 3 to Child 1 + public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) + { + // Arrange + DocumentNavigationQueryService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); + + // Act + var copyAttempt = await ContentEditingService.CopyAsync(nodeToCopy, targetParentKey, false, false, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; + + // Assert + Assert.AreNotEqual(nodeToCopy, copiedItemKey); + + DocumentNavigationQueryService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + + Assert.Multiple(() => + { + if (targetParentKey is null) + { + // Verify the copied node's parent is null (it's been copied to content root) + Assert.IsNull(copiedItemParentKey); + } + else + { + Assert.IsNotNull(copiedItemParentKey); + } + + Assert.AreEqual(targetParentKey, copiedItemParentKey); + Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs new file mode 100644 index 0000000000..2ee6c7cabe --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Creating_Content() + { + // Arrange + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); + var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); + + var createModel = new ContentCreateModel + { + ContentTypeKey = ContentType.Key, + ParentKey = Constants.System.RootKey, // Create node at content root + InvariantName = "Root 2", + }; + + // Act + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the siblings list of the Root once again + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable updatedSiblingsKeys); + List siblingsList = updatedSiblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(initialRootNodeSiblingsCount + 1, siblingsList.Count); + Assert.AreEqual(createdItemKey, siblingsList.First()); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs new file mode 100644 index 0000000000..5e6e655d74 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: Test that the descendants of the node have also been removed from both structures +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public async Task Structure_Updates_When_Deleting_Content(Guid nodeToDelete) + { + // Arrange + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + + // Act + // Deletes the item whether it is in the recycle bin or not + var deleteAttempt = await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + Guid deletedItemKey = deleteAttempt.Result.Key; + + // Assert + var nodeExists = DocumentNavigationQueryService.TryGetDescendantsKeys(deletedItemKey, out _); + var nodeExistsInRecycleBin = DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToDelete, out _); + + Assert.Multiple(() => + { + Assert.AreEqual(nodeToDelete, deletedItemKey); + Assert.IsFalse(nodeExists); + Assert.IsFalse(nodeExistsInRecycleBin); + + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = DocumentNavigationQueryService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + + var descendantExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); + } + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs new file mode 100644 index 0000000000..1f9b819366 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs @@ -0,0 +1,30 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: check that the descendants have also been removed from both structures - navigation and trash +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() + { + // Arrange + Guid nodeToDelete = Child1.Key; + Guid nodeInRecycleBin = Grandchild4.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs new file mode 100644 index 0000000000..078e06de2b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -0,0 +1,39 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 2 to Child 1 + public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) + { + // Arrange + DocumentNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + + // Act + var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + DocumentNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + + // Assert + Assert.Multiple(() => + { + if (targetParentKey is null) + { + Assert.IsNull(updatedParentKey); + } + else + { + Assert.IsNotNull(updatedParentKey); + } + + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs new file mode 100644 index 0000000000..1bd4bd9d83 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -0,0 +1,32 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: also check that initial siblings count's decreased +// TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = Child3.Key; + DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + + // Act + await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure + var nodeExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsTrue(nodeExistsInRecycleBin); + Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); + Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs new file mode 100644 index 0000000000..6d70870a32 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Can_Rebuild() + { + // Arrange + Guid nodeKey = Root.Key; + + // Capture original built state of DocumentNavigationService + DocumentNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + DocumentNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + + // Im-memory navigation structure is empty here + var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); + + // Act + await newDocumentNavigationService.RebuildAsync(); + + // Capture rebuilt state + var nodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); + newDocumentNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); + newDocumentNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newDocumentNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newDocumentNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of DocumentNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); + } + + [Test] + // TODO: Test that you can rebuild bin structure as well + public async Task Bin_Structure_Can_Rebuild() + { + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs new file mode 100644 index 0000000000..3151fb83e4 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs @@ -0,0 +1,50 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +// TODO: test that descendants are also restored in the right place +public partial class DocumentNavigationServiceTests +{ + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 2 to Grandchild 3 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root + public async Task Structure_Updates_When_Restoring_Content(Guid nodeToRestore, Guid? targetParentKey) + { + // Arrange + Guid nodeInRecycleBin = GreatGrandchild1.Key; + + // Move nodes to recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling + await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + + // Act + var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + Guid restoredItemKey = restoreAttempt.Result.Key; + + // Assert + DocumentNavigationQueryService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + + if (targetParentKey is null) + { + Assert.IsNull(restoredItemParentKey); + } + else + { + Assert.IsNotNull(restoredItemParentKey); + Assert.AreNotEqual(initialParentKey, restoredItemParentKey); + } + + Assert.AreEqual(targetParentKey, restoredItemParentKey); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs new file mode 100644 index 0000000000..8ed720000e --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Update.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests +{ + [Test] + public async Task Structure_Does_Not_Update_When_Updating_Content() + { + // Arrange + Guid nodeToUpdate = Root.Key; + + // Capture initial state + DocumentNavigationQueryService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + DocumentNavigationQueryService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); + + var updateModel = new ContentUpdateModel + { + InvariantName = "Updated Root", + }; + + // Act + var updateAttempt = await ContentEditingService.UpdateAsync(nodeToUpdate, updateModel, Constants.Security.SuperUserKey); + Guid updatedItemKey = updateAttempt.Result.Content!.Key; + + // Capture updated state + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + DocumentNavigationQueryService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + DocumentNavigationQueryService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + DocumentNavigationQueryService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); + + // Assert + Assert.Multiple(() => + { + // Verify that the item is still present in the navigation structure + Assert.IsTrue(nodeExists); + + Assert.AreEqual(nodeToUpdate, updatedItemKey); + + // Verify that nothing's changed + Assert.AreEqual(initialParentKey, updatedParentKey); + CollectionAssert.AreEquivalent(initialChildrenKeys, childrenKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialDescendantsKeys, descendantsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialAncestorsKeys, ancestorsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialSiblingsKeys, siblingsKeysAfterUpdate); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs new file mode 100644 index 0000000000..9fdedc5257 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -0,0 +1,90 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class DocumentNavigationServiceTests : DocumentNavigationServiceTestsBase +{ + [SetUp] + public async Task Setup() + { + // Root + // - Child 1 + // - Grandchild 1 + // - Grandchild 2 + // - Child 2 + // - Grandchild 3 + // - Great-grandchild 1 + // - Child 3 + // - Grandchild 4 + + // Doc Type + ContentType = ContentTypeBuilder.CreateSimpleContentType("page", "Page"); + ContentType.Key = new Guid("DD72B8A6-2CE3-47F0-887E-B695A1A5D086"); + ContentType.AllowedAsRoot = true; + ContentType.AllowedTemplates = null; + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; + await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); + + // Content + var rootModel = CreateContentCreateModel("Root", new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF")); + var rootCreateAttempt = await ContentEditingService.CreateAsync(rootModel, Constants.Security.SuperUserKey); + Root = rootCreateAttempt.Result.Content!; + + var child1Model = CreateContentCreateModel("Child 1", new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"), Root.Key); + var child1CreateAttempt = await ContentEditingService.CreateAsync(child1Model, Constants.Security.SuperUserKey); + Child1 = child1CreateAttempt.Result.Content!; + + var grandchild1Model = CreateContentCreateModel("Grandchild 1", new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"), Child1.Key); + var grandchild1CreateAttempt = await ContentEditingService.CreateAsync(grandchild1Model, Constants.Security.SuperUserKey); + Grandchild1 = grandchild1CreateAttempt.Result.Content!; + + var grandchild2Model = CreateContentCreateModel("Grandchild 2", new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"), Child1.Key); + var grandchild2CreateAttempt = await ContentEditingService.CreateAsync(grandchild2Model, Constants.Security.SuperUserKey); + Grandchild2 = grandchild2CreateAttempt.Result.Content!; + + var child2Model = CreateContentCreateModel("Child 2", new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"), Root.Key); + var child2CreateAttempt = await ContentEditingService.CreateAsync(child2Model, Constants.Security.SuperUserKey); + Child2 = child2CreateAttempt.Result.Content!; + + var grandchild3Model = CreateContentCreateModel("Grandchild 3", new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"), Child2.Key); + var grandchild3CreateAttempt = await ContentEditingService.CreateAsync(grandchild3Model, Constants.Security.SuperUserKey); + Grandchild3 = grandchild3CreateAttempt.Result.Content!; + + var greatGrandchild1Model = CreateContentCreateModel("Great-grandchild 1", new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"), Grandchild3.Key); + var greatGrandchild1CreateAttempt = await ContentEditingService.CreateAsync(greatGrandchild1Model, Constants.Security.SuperUserKey); + GreatGrandchild1 = greatGrandchild1CreateAttempt.Result.Content!; + + var child3Model = CreateContentCreateModel("Child 3", new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"), Root.Key); + var child3CreateAttempt = await ContentEditingService.CreateAsync(child3Model, Constants.Security.SuperUserKey); + Child3 = child3CreateAttempt.Result.Content!; + + var grandchild4Model = CreateContentCreateModel("Grandchild 4", new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"), Child3.Key); + var grandchild4CreateAttempt = await ContentEditingService.CreateAsync(grandchild4Model, Constants.Security.SuperUserKey); + Grandchild4 = grandchild4CreateAttempt.Result.Content!; + } + + [Test] + public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() + { + // Arrange + Guid notCreatedRootKey = new Guid("516927E5-8574-497B-B45B-E27EFAB47DE4"); + + // Create node at content root + var createModel = CreateContentCreateModel("Root 2", notCreatedRootKey); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + // Act + var nodeExists = DocumentNavigationQueryService.TryGetParentKey(notCreatedRootKey, out _); + + // Assert + Assert.IsFalse(nodeExists); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs new file mode 100644 index 0000000000..d4325f4674 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTestsBase.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class DocumentNavigationServiceTestsBase : UmbracoIntegrationTest +{ + protected IContentTypeService ContentTypeService => GetRequiredService(); + + // Testing with IContentEditingService as it calls IContentService underneath + protected IContentEditingService ContentEditingService => GetRequiredService(); + + protected IDocumentNavigationQueryService DocumentNavigationQueryService => GetRequiredService(); + + protected IContentType ContentType { get; set; } + + protected IContent Root { get; set; } + + protected IContent Child1 { get; set; } + + protected IContent Grandchild1 { get; set; } + + protected IContent Grandchild2 { get; set; } + + protected IContent Child2 { get; set; } + + protected IContent Grandchild3 { get; set; } + + protected IContent GreatGrandchild1 { get; set; } + + protected IContent Child3 { get; set; } + + protected IContent Grandchild4 { get; set; } + + protected ContentCreateModel CreateContentCreateModel(string name, Guid key, Guid? parentKey = null) + => new() + { + ContentTypeKey = ContentType.Key, + ParentKey = parentKey ?? Constants.System.RootKey, + InvariantName = name, + Key = key, + }; +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs new file mode 100644 index 0000000000..0f36e8b8ec --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -0,0 +1,59 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + [Test] + public async Task Structure_Can_Rebuild() + { + // Arrange + Guid nodeKey = Album.Key; + + // Capture original built state of MediaNavigationService + MediaNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); + MediaNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); + MediaNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); + + // Im-memory navigation structure is empty here + var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out _); + + // Act + await newMediaNavigationService.RebuildAsync(); + + // Capture rebuilt state + var nodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out Guid? parentKeyFromRebuild); + newMediaNavigationService.TryGetChildrenKeys(nodeKey, out IEnumerable childrenKeysFromRebuild); + newMediaNavigationService.TryGetDescendantsKeys(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newMediaNavigationService.TryGetAncestorsKeys(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newMediaNavigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of MediaNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); + } + + [Test] + // TODO: Test that you can rebuild bin structure as well + public async Task Bin_Structure_Can_Rebuild() + { + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs new file mode 100644 index 0000000000..1b3b6551a5 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs @@ -0,0 +1,9 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests +{ + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs new file mode 100644 index 0000000000..ebef7fe046 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.cs @@ -0,0 +1,79 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +public partial class MediaNavigationServiceTests : MediaNavigationServiceTestsBase +{ + [SetUp] + public async Task Setup() + { + // Album + // - Image 1 + // - Sub-album 1 + // - Image 2 + // - Image 3 + // - Sub-album 2 + // - Sub-sub-album 1 + // - Image 4 + + // Media Types + FolderMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Folder); + ImageMediaType = MediaTypeService.Get(Constants.Conventions.MediaTypes.Image); + + // Media + var albumModel = CreateMediaCreateModel("Album", new Guid("1CD97C02-8534-4B72-AE9E-AE52EC94CF31"), FolderMediaType.Key); + var albumCreateAttempt = await MediaEditingService.CreateAsync(albumModel, Constants.Security.SuperUserKey); + Album = albumCreateAttempt.Result.Content!; + + var image1Model = CreateMediaCreateModel("Image 1", new Guid("03976EBE-A942-4F24-9885-9186E99AEF7C"), ImageMediaType.Key, Album.Key); + var image1CreateAttempt = await MediaEditingService.CreateAsync(image1Model, Constants.Security.SuperUserKey); + Image1 = image1CreateAttempt.Result.Content!; + + var subAlbum1Model = CreateMediaCreateModel("Sub-album 1", new Guid("139DC977-E50F-4382-9728-B278C4B7AC6A"), FolderMediaType.Key, Album.Key); + var subAlbum1CreateAttempt = await MediaEditingService.CreateAsync(subAlbum1Model, Constants.Security.SuperUserKey); + SubAlbum1 = subAlbum1CreateAttempt.Result.Content!; + + var image2Model = CreateMediaCreateModel("Image 2", new Guid("3E489C32-9315-42DA-95CE-823D154B09C8"), ImageMediaType.Key, SubAlbum1.Key); + var image2CreateAttempt = await MediaEditingService.CreateAsync(image2Model, Constants.Security.SuperUserKey); + Image2 = image2CreateAttempt.Result.Content!; + + var image3Model = CreateMediaCreateModel("Image 3", new Guid("6176BD70-2CD2-4AEE-A045-084C94E4AFF2"), ImageMediaType.Key, SubAlbum1.Key); + var image3CreateAttempt = await MediaEditingService.CreateAsync(image3Model, Constants.Security.SuperUserKey); + Image3 = image3CreateAttempt.Result.Content!; + + var subAlbum2Model = CreateMediaCreateModel("Sub-album 2", new Guid("DBCAFF2F-BFA4-4744-A948-C290C432D564"), FolderMediaType.Key, Album.Key); + var subAlbum2CreateAttempt = await MediaEditingService.CreateAsync(subAlbum2Model, Constants.Security.SuperUserKey); + SubAlbum2 = subAlbum2CreateAttempt.Result.Content!; + + var subSubAlbum1Model = CreateMediaCreateModel("Sub-sub-album 1", new Guid("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB"), FolderMediaType.Key, SubAlbum2.Key); + var subSubAlbum1CreateAttempt = await MediaEditingService.CreateAsync(subSubAlbum1Model, Constants.Security.SuperUserKey); + SubSubAlbum1 = subSubAlbum1CreateAttempt.Result.Content!; + + var image4Model = CreateMediaCreateModel("Image 4", new Guid("62BCE72F-8C18-420E-BCAC-112B5ECC95FD"), ImageMediaType.Key, SubSubAlbum1.Key); + var image4CreateAttempt = await MediaEditingService.CreateAsync(image4Model, Constants.Security.SuperUserKey); + Image4 = image4CreateAttempt.Result.Content!; + } + + [Test] + public async Task Structure_Does_Not_Update_When_Scope_Is_Not_Completed() + { + // Arrange + Guid notCreatedAlbumKey = new Guid("860EE748-BC7E-4A13-A1D9-C9160B25AD6E"); + + // Create node at media root + var createModel = CreateMediaCreateModel("Album 2", notCreatedAlbumKey, FolderMediaType.Key); + + using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + { + await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + } + + // Act + var nodeExists = MediaNavigationQueryService.TryGetParentKey(notCreatedAlbumKey, out _); + + // Assert + Assert.IsFalse(nodeExists); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs new file mode 100644 index 0000000000..6d9e693320 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTestsBase.cs @@ -0,0 +1,51 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public abstract class MediaNavigationServiceTestsBase : UmbracoIntegrationTest +{ + protected IMediaTypeService MediaTypeService => GetRequiredService(); + + // Testing with IMediaEditingService as it calls IMediaService underneath + protected IMediaEditingService MediaEditingService => GetRequiredService(); + + protected IMediaNavigationQueryService MediaNavigationQueryService => GetRequiredService(); + + protected IMediaType FolderMediaType { get; set; } + + protected IMediaType ImageMediaType { get; set; } + + protected IMedia Album { get; set; } + + protected IMedia Image1 { get; set; } + + protected IMedia SubAlbum1 { get; set; } + + protected IMedia Image2 { get; set; } + + protected IMedia Image3 { get; set; } + + protected IMedia SubAlbum2 { get; set; } + + protected IMedia SubSubAlbum1 { get; set; } + + protected IMedia Image4 { get; set; } + + protected MediaCreateModel CreateMediaCreateModel(string name, Guid key, Guid mediaTypeKey, Guid? parentKey = null) + => new() + { + ContentTypeKey = mediaTypeKey, + ParentKey = parentKey ?? Constants.System.RootKey, + InvariantName = name, + Key = key, + }; +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index b7ab21b062..de8274a94f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -154,6 +154,57 @@ MediaTypeEditingServiceTests.cs + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + DocumentNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + + + MediaNavigationServiceTests.cs + diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs new file mode 100644 index 0000000000..7d9a2e8397 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/ContentNavigationServiceBaseTests.cs @@ -0,0 +1,996 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +public class ContentNavigationServiceBaseTests +{ + private TestContentNavigationService _navigationService; + + private Guid Root { get; set; } + + private Guid Child1 { get; set; } + + private Guid Grandchild1 { get; set; } + + private Guid Grandchild2 { get; set; } + + private Guid Child2 { get; set; } + + private Guid Grandchild3 { get; set; } + + private Guid GreatGrandchild1 { get; set; } + + private Guid Child3 { get; set; } + + private Guid Grandchild4 { get; set; } + + [SetUp] + public void Setup() + { + // Root + // - Child 1 + // - Grandchild 1 + // - Grandchild 2 + // - Child 2 + // - Grandchild 3 + // - Great-grandchild 1 + // - Child 3 + // - Grandchild 4 + + _navigationService = new TestContentNavigationService( + Mock.Of(), + Mock.Of()); + + Root = new Guid("E48DD82A-7059-418E-9B82-CDD5205796CF"); + _navigationService.Add(Root); + + Child1 = new Guid("C6173927-0C59-4778-825D-D7B9F45D8DDE"); + _navigationService.Add(Child1, Root); + + Grandchild1 = new Guid("E856AC03-C23E-4F63-9AA9-681B42A58573"); + _navigationService.Add(Grandchild1, Child1); + + Grandchild2 = new Guid("A1B1B217-B02F-4307-862C-A5E22DB729EB"); + _navigationService.Add(Grandchild2, Child1); + + Child2 = new Guid("60E0E5C4-084E-4144-A560-7393BEAD2E96"); + _navigationService.Add(Child2, Root); + + Grandchild3 = new Guid("D63C1621-C74A-4106-8587-817DEE5FB732"); + _navigationService.Add(Grandchild3, Child2); + + GreatGrandchild1 = new Guid("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7"); + _navigationService.Add(GreatGrandchild1, Grandchild3); + + Child3 = new Guid("B606E3FF-E070-4D46-8CB9-D31352029FDF"); + _navigationService.Add(Child3, Root); + + Grandchild4 = new Guid("F381906C-223C-4466-80F7-B63B4EE073F8"); + _navigationService.Add(Grandchild4, Child3); + } + + [Test] + public void Cannot_Get_Parent_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.TryGetParentKey(nonExistingKey, out Guid? parentKey); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsNull(parentKey); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 + public void Can_Get_Parent_From_Existing_Content_Key(Guid childKey, Guid? expectedParentKey) + { + // Act + var result = _navigationService.TryGetParentKey(childKey, out Guid? parentKey); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + + if (expectedParentKey is null) + { + Assert.IsNull(parentKey); + } + else + { + Assert.IsNotNull(parentKey); + Assert.AreEqual(expectedParentKey, parentKey); + } + }); + } + + [Test] + public void Cannot_Get_Children_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.TryGetChildrenKeys(nonExistingKey, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(childrenKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 3)] // Root - Child 1, Child 2, Child 3 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Grandchild 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Children_From_Existing_Content_Key(Guid parentKey, int childrenCount) + { + // Act + var result = _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(childrenCount, childrenKeys.Count()); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + new[] + { + "C6173927-0C59-4778-825D-D7B9F45D8DDE", "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "B606E3FF-E070-4D46-8CB9-D31352029FDF" + })] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", + new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", new[] { "D63C1621-C74A-4106-8587-817DEE5FB732" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public void Can_Get_Children_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] children) + { + // Arrange + Guid[] expectedChildren = Array.ConvertAll(children, Guid.Parse); + + // Act + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + + // Assert + for (var i = 0; i < expectedChildren.Length; i++) + { + Assert.AreEqual(expectedChildren[i], childrenList.ElementAt(i)); + } + } + + [Test] + public void Cannot_Get_Descendants_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.TryGetDescendantsKeys(nonExistingKey, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(descendantsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + 8)] // Root - Child 1, Grandchild 1, Grandchild 2, Child 2, Grandchild 3, Great-grandchild 1, Child 3, Grandchild 4 + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Grandchild 1, Grandchild 2 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 0)] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Grandchild 3, Great-grandchild 1 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 1)] // Grandchild 3 - Great-grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Grandchild 4 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Descendants_From_Existing_Content_Key(Guid parentKey, int descendantsCount) + { + // Act + var result = _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable descendantsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(descendantsCount, descendantsKeys.Count()); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", + new[] + { + "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E856AC03-C23E-4F63-9AA9-681B42A58573", + "A1B1B217-B02F-4307-862C-A5E22DB729EB", "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", + "B606E3FF-E070-4D46-8CB9-D31352029FDF", "F381906C-223C-4466-80F7-B63B4EE073F8" + })] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", + new[] { "E856AC03-C23E-4F63-9AA9-681B42A58573", "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new string[0])] // Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", + new[] { "D63C1621-C74A-4106-8587-817DEE5FB732", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", new[] { "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7" })] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", new string[0])] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", new[] { "F381906C-223C-4466-80F7-B63B4EE073F8" })] // Child 3 + public void Can_Get_Descendants_From_Existing_Content_Key_In_Correct_Order(Guid parentKey, string[] descendants) + { + // Arrange + Guid[] expectedDescendants = Array.ConvertAll(descendants, Guid.Parse); + + // Act + _navigationService.TryGetDescendantsKeys(parentKey, out IEnumerable descendantsKeys); + List descendantsList = descendantsKeys.ToList(); + + // Assert + for (var i = 0; i < expectedDescendants.Length; i++) + { + Assert.AreEqual(expectedDescendants[i], descendantsList.ElementAt(i)); + } + } + + [Test] + public void Cannot_Get_Ancestors_From_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.TryGetAncestorsKeys(nonExistingKey, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(ancestorsKeys); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 1)] // Child 1 - Root + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 2)] // Grandchild 1 - Child 1, Root + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 2)] // Grandchild 2 - Child 1, Root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 1)] // Child 2 - Root + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 2)] // Grandchild 3 - Child 2, Root + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 3)] // Great-grandchild 1 - Grandchild 3, Child 2, Root + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 3 - Root + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 2)] // Grandchild 4 - Child 3, Root + public void Can_Get_Ancestors_From_Existing_Content_Key(Guid childKey, int ancestorsCount) + { + // Act + var result = _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable ancestorsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(ancestorsCount, ancestorsKeys.Count()); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", + new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF" })] // Grandchild 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", + new[] + { + "D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96", + "E48DD82A-7059-418E-9B82-CDD5205796CF" + })] // Great-grandchild 1 + public void Can_Get_Ancestors_From_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] ancestors) + { + // Arrange + Guid[] expectedAncestors = Array.ConvertAll(ancestors, Guid.Parse); + + // Act + _navigationService.TryGetAncestorsKeys(childKey, out IEnumerable ancestorsKeys); + List ancestorsList = ancestorsKeys.ToList(); + + // Assert + for (var i = 0; i < expectedAncestors.Length; i++) + { + Assert.AreEqual(expectedAncestors[i], ancestorsList.ElementAt(i)); + } + } + + [Test] + public void Cannot_Get_Siblings_Of_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.TryGetSiblingsKeys(nonExistingKey, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(result); + Assert.IsEmpty(siblingsKeys); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Existing_Content_Key_Without_Self() + { + // Arrange + Guid nodeKey = Child1; + + // Act + var result = _navigationService.TryGetSiblingsKeys(nodeKey, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.IsNotEmpty(siblingsList); + Assert.IsFalse(siblingsList.Contains(nodeKey)); + }); + } + + [Test] + public void Can_Get_Siblings_Of_Existing_Content_Key_At_Content_Root() + { + // Arrange + Guid anotherRoot = new Guid("716380B9-DAA9-4930-A461-95EF39EBAB41"); + _navigationService.Add(anotherRoot); + + // Act + _navigationService.TryGetSiblingsKeys(anotherRoot, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(1, siblingsList.Count); + Assert.AreEqual(Root, siblingsList.First()); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", 0)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 1 - Child 2, Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", 1)] // Grandchild 1 - Grandchild 2 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", 1)] // Grandchild 2 - Grandchild 1 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Child 2 - Child 1, Child 3 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", 0)] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 0)] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", 2)] // Child 3 - Child 1, Child 2 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", 0)] // Grandchild 4 + public void Can_Get_Siblings_Of_Existing_Content_Key(Guid key, int siblingsCount) + { + // Act + var result = _navigationService.TryGetSiblingsKeys(key, out IEnumerable siblingsKeys); + + // Assert + Assert.Multiple(() => + { + Assert.IsTrue(result); + Assert.AreEqual(siblingsCount, siblingsKeys.Count()); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", new string[0])] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", new[] { "60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Child 1 - Child 2, Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", new[] { "A1B1B217-B02F-4307-862C-A5E22DB729EB" })] // Grandchild 1 - Grandchild 2 + public void Can_Get_Siblings_Of_Existing_Content_Key_In_Correct_Order(Guid childKey, string[] siblings) + { + // Arrange + Guid[] expectedSiblings = Array.ConvertAll(siblings, Guid.Parse); + + // Act + _navigationService.TryGetSiblingsKeys(childKey, out IEnumerable siblingsKeys); + List siblingsList = siblingsKeys.ToList(); + + // Assert + for (var i = 0; i < expectedSiblings.Length; i++) + { + Assert.AreEqual(expectedSiblings[i], siblingsList.ElementAt(i)); + } + } + + [Test] + public void Cannot_Move_Node_To_Bin_When_Non_Existing_Content_Key() + { + // Arrange + var nonExistingKey = Guid.NewGuid(); + + // Act + var result = _navigationService.MoveToBin(nonExistingKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Can_Move_Node_To_Bin(Guid keyOfNodeToRemove) + { + // Act + var result = _navigationService.MoveToBin(keyOfNodeToRemove); + + // Assert + Assert.IsTrue(result); + + var nodeExists = _navigationService.TryGetParentKey(keyOfNodeToRemove, out Guid? parentKey); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsNull(parentKey); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Moving_Node_To_Bin_Removes_Its_Descendants_As_Well(Guid keyOfNodeToRemove) + { + // Arrange + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); + + // Act + var result = _navigationService.MoveToBin(keyOfNodeToRemove); + + // Assert + Assert.IsTrue(result); + + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable descendantsKeys); + + Assert.Multiple(() => + { + Assert.AreEqual(0, descendantsKeys.Count()); + + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = _navigationService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + } + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Moving_Node_To_Bin_Adds_It_To_Recycle_Bin_Root(Guid keyOfNodeToRemove) + { + // Act + _navigationService.MoveToBin(keyOfNodeToRemove); + + // Assert + var nodeExistsInBin = _navigationService.TryGetParentKeyInBin(keyOfNodeToRemove, out Guid? parentKeyInBin); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExistsInBin); + Assert.IsNull(parentKeyInBin); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Moving_Node_To_Bin_Adds_Its_Descendants_To_Recycle_Bin_As_Well(Guid keyOfNodeToRemove) + { + // Arrange + _navigationService.TryGetDescendantsKeys(keyOfNodeToRemove, out IEnumerable initialDescendantsKeys); + List initialDescendantsList = initialDescendantsKeys.ToList(); + + // Act + _navigationService.MoveToBin(keyOfNodeToRemove); + + // Assert + var nodeExistsInBin = _navigationService.TryGetDescendantsKeysInBin(keyOfNodeToRemove, out IEnumerable descendantsKeysInBin); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExistsInBin); + CollectionAssert.AreEqual(initialDescendantsList, descendantsKeysInBin); + + foreach (Guid descendant in initialDescendantsList) + { + _navigationService.TryGetParentKeyInBin(descendant, out Guid? parentKeyInBin); + Assert.IsNotNull(parentKeyInBin); // The descendant kept its initial parent + } + }); + } + + [Test] + public void Cannot_Add_Node_When_Parent_Does_Not_Exist() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + var nonExistentParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Add(newNodeKey, nonExistentParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Add_When_Node_With_The_Same_Key_Already_Exists() + { + // Act + var result = _navigationService.Add(Child1); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Can_Add_Node_To_Content_Root() + { + // Arrange + var newNodeKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Add(newNodeKey); // parentKey is null + + // Assert + Assert.IsTrue(result); + + var nodeExists = _navigationService.TryGetParentKey(newNodeKey, out Guid? parentKey); + + Assert.Multiple(() => + { + Assert.IsTrue(nodeExists); + Assert.IsNull(parentKey); + }); + } + + [Test] + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1 + public void Can_Add_Node_To_Parent(Guid parentKey) + { + // Arrange + var newNodeKey = Guid.NewGuid(); + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable currentChildrenKeys); + var currentChildrenCount = currentChildrenKeys.Count(); + + // Act + var result = _navigationService.Add(newNodeKey, parentKey); + + // Assert + Assert.IsTrue(result); + + _navigationService.TryGetChildrenKeys(parentKey, out IEnumerable newChildrenKeys); + var newChildrenList = newChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.AreEqual(currentChildrenCount + 1, newChildrenList.Count); + Assert.IsTrue(newChildrenList.Any(childKey => childKey == newNodeKey)); + }); + } + + [Test] + public void Cannot_Move_Node_When_Target_Parent_Does_Not_Exist() + { + // Arrange + Guid nodeToMove = Child1; + var nonExistentTargetParentKey = Guid.NewGuid(); + + // Act + var result = _navigationService.Move(nodeToMove, nonExistentTargetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Move_Node_That_Does_Not_Exist() + { + // Arrange + var nonExistentNodeKey = Guid.NewGuid(); + Guid targetParentKey = Child1; + + // Act + var result = _navigationService.Move(nonExistentNodeKey, targetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Move_Node_To_Itself() + { + // Arrange + Guid nodeToMove = Child1; + + // Act + var result = _navigationService.Move(nodeToMove, nodeToMove); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Can_Move_Node_To_Content_Root() + { + // Arrange + Guid nodeToMove = Child1; + + // Act + var result = _navigationService.Move(nodeToMove); // parentKey is null + + // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is null (moved to content root) + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + + Assert.IsNull(newParentKey); + } + + [Test] + public void Can_Move_Node_To_Existing_Target_Parent() + { + // Arrange + Guid nodeToMove = Grandchild4; + Guid targetParentKey = Child1; + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + + Assert.Multiple(() => + { + Assert.IsNotNull(newParentKey); + Assert.AreEqual(targetParentKey, newParentKey); + }); + } + + [Test] + public void Moved_Node_Has_Updated_Parent() + { + // Arrange + Guid nodeToMove = Grandchild1; + Guid targetParentKey = Child2; + _navigationService.TryGetParentKey(nodeToMove, out Guid? oldParentKey); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToMove, out Guid? newParentKey); + + Assert.Multiple(() => + { + Assert.IsNotNull(newParentKey); + Assert.AreEqual(targetParentKey, newParentKey); + + // Verify that the new parent is different from the old one + Assert.AreNotEqual(oldParentKey, targetParentKey); + }); + } + + [Test] + public void Moved_Node_Is_Removed_From_Its_Current_Parent() + { + // Arrange + Guid nodeToMove = Grandchild3; + Guid targetParentKey = Child3; + _navigationService.TryGetParentKey(nodeToMove, out Guid? oldParentKey); + _navigationService.TryGetChildrenKeys(oldParentKey!.Value, out IEnumerable oldParentChildrenKeys); + var oldParentChildrenCount = oldParentChildrenKeys.Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node is removed from its old parent's children list + _navigationService.TryGetChildrenKeys(oldParentKey.Value, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + + Assert.Multiple(() => + { + CollectionAssert.DoesNotContain(childrenList, nodeToMove); + Assert.AreEqual(oldParentChildrenCount - 1, childrenList.Count); + }); + } + + [Test] + public void Moved_Node_Is_Added_To_Its_New_Parent() + { + // Arrange + Guid nodeToMove = Grandchild2; + Guid targetParentKey = Child2; + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); + var targetParentChildrenCount = targetParentChildrenKeys.Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node is added to its new parent's children list + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + + Assert.Multiple(() => + { + CollectionAssert.Contains(childrenList, nodeToMove); + Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); + }); + } + + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null, 1)] // Child 3 to content root + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE", 2)] // Child 2 to Child 1 + public void Moved_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToMove, Guid? targetParentKey, int initialDescendantsCount) + { + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify that the number of descendants remain the same after moving the node + _navigationService.TryGetDescendantsKeys(nodeToMove, out IEnumerable descendantsKeys); + var descendantsCountAfterMove = descendantsKeys.Count(); + + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterMove); + } + + [Test] + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "A1B1B217-B02F-4307-862C-A5E22DB729EB", 0)] // Child 3 to Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "B606E3FF-E070-4D46-8CB9-D31352029FDF", 1)] // Child 2 to Child 3 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 2)] // Grandchild 1 to Child 2 + public void Number_Of_Target_Parent_Descendants_Updates_When_Moving_Node_With_Descendants(Guid nodeToMove, Guid targetParentKey, int initialDescendantsCountOfTargetParent) + { + // Arrange + // Get the number of descendants of the node to move + _navigationService.TryGetDescendantsKeys(nodeToMove, out IEnumerable descendantsKeys); + var descendantsCountOfNodeToMove = descendantsKeys.Count(); + + // Act + var result = _navigationService.Move(nodeToMove, targetParentKey); + + // Assert + Assert.IsTrue(result); + + _navigationService.TryGetDescendantsKeys(targetParentKey, out IEnumerable updatedTargetParentDescendantsKeys); + var updatedDescendantsCountOfTargetParent = updatedTargetParentDescendantsKeys.Count(); + + // Verify the number of descendants of the target parent has increased by the number of descendants of the moved node plus the node itself + Assert.AreEqual(initialDescendantsCountOfTargetParent + descendantsCountOfNodeToMove + 1, updatedDescendantsCountOfTargetParent); + } + + [Test] + public void Cannot_Restore_Node_When_Target_Parent_Does_Not_Exist() + { + // Arrange + Guid nodeToRestore = Grandchild1; + var nonExistentTargetParentKey = Guid.NewGuid(); + _navigationService.MoveToBin(nodeToRestore); + + // Act + var result = _navigationService.RestoreFromBin(nodeToRestore, nonExistentTargetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void Cannot_Restore_Node_That_Does_Not_Exist() + { + // Arrange + Guid notDeletedNodeKey = Grandchild4; + Guid targetParentKey = Child3; + + // Act + var result = _navigationService.RestoreFromBin(notDeletedNodeKey, targetParentKey); + + // Assert + Assert.IsFalse(result); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null)] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 1 + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 + [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 2 + [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 2 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 3 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "D63C1621-C74A-4106-8587-817DEE5FB732")] // Great-grandchild 1 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Grandchild 4 + public void Can_Restore_Node_To_Existing_Target_Parent(Guid nodeToRestore, Guid? targetParentKey) + { + // Arrange + _navigationService.MoveToBin(nodeToRestore); + + // Act + var result = _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node's new parent is updated + _navigationService.TryGetParentKey(nodeToRestore, out Guid? parentKeyAfterRestore); + + Assert.Multiple(() => + { + if (targetParentKey is null) + { + Assert.IsNull(parentKeyAfterRestore); + } + else + { + Assert.IsNotNull(parentKeyAfterRestore); + } + + Assert.AreEqual(targetParentKey, parentKeyAfterRestore); + }); + } + + [Test] + [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Grandchild 1 to Child 1 + [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Great-grandchild 1 to Child 2 + [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "E48DD82A-7059-418E-9B82-CDD5205796CF")] // Child 3 to Root + public void Restored_Node_Is_Added_To_Its_Target_Parent(Guid nodeToRestore, Guid targetParentKey) + { + // Arrange + _navigationService.MoveToBin(nodeToRestore); + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable targetParentChildrenKeys); + var targetParentChildrenCount = targetParentChildrenKeys.Count(); + + // Act + var result = _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); + + // Assert + Assert.IsTrue(result); + + // Verify the node is added to its target parent's children list + _navigationService.TryGetChildrenKeys(targetParentKey, out IEnumerable childrenKeys); + List childrenList = childrenKeys.ToList(); + + Assert.Multiple(() => + { + CollectionAssert.Contains(childrenList, nodeToRestore); + Assert.AreEqual(targetParentChildrenCount + 1, childrenList.Count); + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1 + [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4 + public void Restored_Node_And_Its_Descendants_Are_Removed_From_Bin(Guid nodeToRestore) + { + // Arrange + _navigationService.MoveToBin(nodeToRestore); + _navigationService.TryGetDescendantsKeysInBin(nodeToRestore, out IEnumerable descendantsKeysInBin); + + // Act + _navigationService.RestoreFromBin(nodeToRestore); + + // Assert + var nodeExistsInBin = _navigationService.TryGetParentKeyInBin(nodeToRestore, out Guid? parentKeyInBinAfterRestore); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExistsInBin); + Assert.IsNull(parentKeyInBinAfterRestore); + + foreach (Guid descendant in descendantsKeysInBin) + { + var descendantExistsInBin = _navigationService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInBin); + } + }); + } + + [Test] + [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF", null, 8)] // Root to content root + [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE", "56E29EA9-E224-4210-A59F-7C2C5C0C5CC7", 2)] // Child 1 to Great-grandchild 1 + [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8", "60E0E5C4-084E-4144-A560-7393BEAD2E96", 0)] // Grandchild 4 to Child 2 + public void Restored_Node_Has_The_Same_Amount_Of_Descendants(Guid nodeToRestore, Guid? targetParentKey, int initialDescendantsCount) + { + // Arrange + _navigationService.MoveToBin(nodeToRestore); + + // Act + _navigationService.RestoreFromBin(nodeToRestore, targetParentKey); + + // Assert + // Verify that the number of descendants remain the same after restoring the node + _navigationService.TryGetDescendantsKeys(nodeToRestore, out IEnumerable restoredDescendantsKeys); + var descendantsCountAfterRestore = restoredDescendantsKeys.Count(); + + Assert.AreEqual(initialDescendantsCount, descendantsCountAfterRestore); + } +} + +internal class TestContentNavigationService : ContentNavigationServiceBase +{ + public TestContentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository) + : base(coreScopeProvider, navigationRepository) + { + } + + // Not needed for testing here + public override Task RebuildAsync() => Task.CompletedTask; + + // Not needed for testing here + public override Task RebuildBinAsync() => Task.CompletedTask; +} From dcd6f1fbf4938eac0988b41a766072ee115bea2c Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 4 Sep 2024 15:35:08 +0200 Subject: [PATCH 11/38] Fixed install issue --- .../NavigationInitializationHostedService.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs index 274158d40d..a11b55b7b8 100644 --- a/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs +++ b/src/Umbraco.Core/Services/Navigation/NavigationInitializationHostedService.cs @@ -8,17 +8,27 @@ namespace Umbraco.Cms.Core.Services.Navigation; /// public sealed class NavigationInitializationHostedService : IHostedLifecycleService { + private readonly IRuntimeState _runtimeState; private readonly IDocumentNavigationManagementService _documentNavigationManagementService; private readonly IMediaNavigationManagementService _mediaNavigationManagementService; - public NavigationInitializationHostedService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService) + public NavigationInitializationHostedService( + IRuntimeState runtimeState, + IDocumentNavigationManagementService documentNavigationManagementService, + IMediaNavigationManagementService mediaNavigationManagementService) { + _runtimeState = runtimeState; _documentNavigationManagementService = documentNavigationManagementService; _mediaNavigationManagementService = mediaNavigationManagementService; } public async Task StartingAsync(CancellationToken cancellationToken) { + if(_runtimeState.Level < RuntimeLevel.Upgrade) + { + return; + } + await _documentNavigationManagementService.RebuildAsync(); await _documentNavigationManagementService.RebuildBinAsync(); await _mediaNavigationManagementService.RebuildAsync(); From 2704d4a34af9716e41ea50e35356b7323d602958 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Tue, 10 Sep 2024 00:49:18 +0900 Subject: [PATCH 12/38] V15: Hybrid Caching (#16938) * Update to dotnet 9 and update nuget packages * Update umbraco code version * Update Directory.Build.props Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Include preview version in pipeline * update template projects * update global json with specific version * Update version.json to v15 * Rename TrimStart and TrimEnd to string specific * Rename to Exact * Update global.json Co-authored-by: Ronald Barendse * Remove includePreviewVersion * Rename to trim exact * Add new Hybridcache project * Add tests * Start implementing PublishedContent.cs * Implement repository for content * Refactor to use async everywhere * Add cache refresher * make public as needed for serialization * Use content type cache to get content type out * Refactor to use ContentCacheNode model, that goes in the memory cache * Remove content node kit as its not needed * Implement tests for ensuring caching * Implement better asserts * Implement published property * Refactor to use mapping * Rename to document tests * Update to test properties * Create more tests * Refactor mock tests into own file * Update property test * Fix published version of content * Change default cache level to elements * Refactor to always have draft * Refactor to not use PublishedModelFactory * Added tests * Added and updated tests * Fixed tests * Don't return empty object with id * More tests * Added key * Another key * Refactor CacheService to be responsible for using the hybrid cache * Use notification handler to remove deleted content from cache * Add more tests for missing functions * Implement missing methods * Remove HasContent as it pertains to routing * Fik up test * formatting * refactor variable names * Implement variant tests * Map all the published content properties * Get item out of cache first, to assert updated * Implement member cache * Add member test * Implement media cache * Implement property tests for media tests * Refactor tests to use extension method * Add more media tests * Refactor properties to no longer have element caching * Don't use property cache level * Start implementing seeding * Only seed when main * Add Immutable for performance * Implement permanent seeding of content * Implement cache settings * Implement tests for seeding * Update package version * start refactoring nurepo * Refactor so draft & published nodes are cached individually * Refactor RefreshContent to take node instead of IContent * Refactor media to also use cache nodes * Remove member from repo as it isn't cached * Refactor media to not include preview, as media has no draft * create new benchmark project * POC Integration benchmarks with custom api controllers * Start implementing content picker tests * Implement domain cache * Rework content cache to implement interface * Start implementing elements cache * Implement published snapshot service * Publish snapshot tests * Use snapshot for elements cache * Create test proving we don't clear cache when updating content picker * Clear entire elements cache * Remove properties from element cache, when content gets updated. * Rename methods to async * Refactor to use old cache interfaces instead of new ones * Remove snapshot, as it is no longer needed * Fix tests building * Refactor domaincache to not have snapshots * Delete benchmarks * Delete benchmarks * Add HybridCacheProject to Umbraco * Add comment to route value transformer * Implement is draft * remove snapshot from property * V15 updated the hybrid caching integration tests to use ContentEditingService (#16947) * Added builder extension withParentKey * Created builder with ContentEditingService * Added usage of the ContentEditingService to SETUP * Started using ContentEditingService builder in tests * Updated builder extensions * Fixed builder * Clean up * Clean up, not done * Added Ids * Remove entries from cache on delete * Fix up seeding logic * Don't register hybrid cache twice * Change seeded entry options * Update hybrid cache package * Fix up published property to work with delivery api again * Fix dependency injection to work with tests * Fix naming * Dont make caches nullable * Make content node sealed * Remove path and other unused from content node * Remove hacky 2 phase ctor * Refactor to actually set content templates * Remove umbraco context * Remove "HasBy" methods * rename property data * Delete obsolete legacy stuff * Add todo for making expiration configurable * Add todo in UmbracoContext * Add clarifying comment in content factory * Remove xml stuff from published property * Fix according to review * Make content type cache injectible * Make content type cache injectible * Rename to database cache repository * Rename to document cache * Add TODO * Refactor to async * Rename to async * Make everything async * Remove duplicate line from json schema * Move Hybrid cache project * Remove leftover file * Refactor to use keys * Refactor published content to no longer have content data, as it is on the node itself * Refactor to member to use proper content node ctor * Move tests to own folder * Add immutable objects to property and content data for performance * Make property data public * Fix member caching to be singleton * Obsolete GetContentType * Remove todo * Fix naming * Fix lots of exposed errors due to scope test * Add final scope tests * Rename to document cache service * Rename test files * Create new doc type tests * Add ignore to tests * Start implementing refresh for content type save * Clear contenttype cache when contenttype is updated * Fix test Teh contenttype is not upated unless the property is dirty * Use init for ContentSourceDto * Fix get by key in PublishedContentTypeCache * Remove ContentType from PublishedContentTypeCache when contenttype is deleted * Update to preview 7 * Fix versions * Increase timeout for sqlite integration tests * Undo timeout increase * Try and undo init change to ContentSourceDto * That wasn't it chief * Try and make DomainAndUrlsTests non NonParallelizable * Update versions * Only run cache tests on linux for now --------- Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Ronald Barendse Co-authored-by: Andreas Zerbst Co-authored-by: Sven Geusens Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: nikolajlauridsen --- Directory.Packages.props | 42 +- global.json | 2 +- .../UmbracoBuilder.BackOffice.cs | 1 + .../Umbraco.Cms.Api.Management.csproj | 1 + .../Implement/DomainCacheRefresher.cs | 4 +- src/Umbraco.Core/Constants-Configuration.cs | 1 + .../UmbracoBuilder.Configuration.cs | 5 +- src/Umbraco.Core/Models/CacheSettings.cs | 13 + .../PublishedContent/IPublishedMember.cs | 22 + .../PublishedContent/IPublishedMemberCache.cs | 10 +- .../PublishedContent/PublishedPropertyType.cs | 2 +- .../ContentPickerPropertyEditor.cs | 2 +- .../PropertyEditors/PropertyCacheLevel.cs | 1 + .../ContentPickerValueConverter.cs | 11 +- .../PublishedCache/ICacheManager.cs | 37 + .../PublishedCache/IDomainCacheService.cs | 31 + .../PublishedCache/IPublishedCache.cs | 27 +- .../PublishedCache/IPublishedContentCache.cs | 23 + .../IPublishedContentTypeCache.cs | 47 + .../PublishedCache/IPublishedMediaCache.cs | 17 + .../Internal/InternalPublishedContentCache.cs | 11 + .../Templates/HtmlLocalLinkParser.cs | 30 +- src/Umbraco.Core/Web/IUmbracoContext.cs | 1 + .../Persistence/NPocoDatabaseExtensions.cs | 17 +- .../Persistence/NPocoSqlExtensions.cs | 15 + .../PublishedContentTypeCache.cs | 35 +- .../CacheManager.cs | 27 + .../ContentCacheNode.cs | 24 + .../ContentData.cs | 45 + .../ContentNode.cs | 61 ++ .../CultureVariation.cs | 29 + .../UmbracoBuilderExtensions.cs | 67 ++ .../DocumentCache.cs | 64 ++ .../DomainCache.cs | 34 + .../ElementsDictionaryAppCache.cs | 7 + .../Factories/CacheNodeFactory.cs | 120 +++ .../Factories/ICacheNodeFactory.cs | 9 + .../Factories/IPublishedContentFactory.cs | 12 + .../Factories/PublishedContentFactory.cs | 159 ++++ .../IElementsCache.cs | 7 + .../MediaCache.cs | 56 ++ .../MemberCache.cs | 27 + .../CacheRefreshingNotificationHandler.cs | 124 +++ .../SeedingNotificationHandler.cs | 21 + .../Persistence/ContentSourceDto.cs | 67 ++ .../Persistence/DatabaseCacheRepository.cs | 895 ++++++++++++++++++ .../Persistence/IDatabaseCacheRepository.cs | 57 ++ .../PropertyData.cs | 43 + .../PublishedContent.cs | 195 ++++ .../PublishedMember.cs | 38 + .../PublishedProperty.cs | 330 +++++++ .../Serialization/ContentCacheDataModel.cs | 30 + .../ContentCacheDataSerializationResult.cs | 47 + .../ContentCacheDataSerializerEntityType.cs | 9 + .../IContentCacheDataSerializer.cs | 22 + .../IContentCacheDataSerializerFactory.cs | 15 + .../JsonContentNestedDataSerializer.cs | 39 + .../JsonContentNestedDataSerializerFactory.cs | 8 + .../Serialization/LazyCompressedString.cs | 105 ++ ...ctionaryStringInternIgnoreCaseFormatter.cs | 27 + .../MsgPackContentNestedDataSerializer.cs | 147 +++ ...gPackContentNestedDataSerializerFactory.cs | 69 ++ .../Services/DocumentCacheService.cs | 174 ++++ .../Services/DomainCacheService.cs | 111 +++ .../Services/IDocumentCacheService.cs | 21 + .../Services/IMediaCacheService.cs | 17 + .../Services/IMemberCacheService.cs | 9 + .../Services/MediaCacheService.cs | 120 +++ .../Services/MemberCacheService.cs | 15 + .../Umbraco.PublishedCache.HybridCache.csproj | 35 + .../ContentCache.cs | 6 + .../MediaCache.cs | 6 + .../MemberCache.cs | 7 +- .../Persistence/NuCacheContentRepository.cs | 1 + .../PublishedMember.cs | 2 +- .../Routing/UmbracoRouteValueTransformer.cs | 1 + tests/Directory.Packages.props | 16 +- .../Builders/ContentEditingBuilder.cs | 172 ++++ .../ContentEditingBuilderExtensions.cs | 58 ++ .../IWithContentTypeKeyBuilder.cs | 6 + .../IWithInvariantNameBuilder.cs | 6 + .../IWithInvariantPropertiesBuilder.cs | 8 + .../IWithParentKeyBuilder.cs | 9 + .../IWithTemplateKeyBuilder.cs | 6 + .../IWithVariantsBuilder.cs | 8 + .../UmbracoTestServerTestBase.cs | 3 +- ...mbracoIntegrationTestWithContentEditing.cs | 133 +++ .../DocumentHybridCacheDocumentTypeTests.cs | 82 ++ .../DocumentHybridCacheMockTests.cs | 205 ++++ .../DocumentHybridCachePropertyTest.cs | 185 ++++ .../DocumentHybridCacheScopeTests.cs | 85 ++ .../DocumentHybridCacheTests.cs | 518 ++++++++++ .../DocumentHybridCacheVariantsTests.cs | 193 ++++ .../MediaHybridCacheTests.cs | 239 +++++ .../MemberHybridCacheTests.cs | 81 ++ .../Umbraco.Tests.Integration.csproj | 1 + .../UrlAndDomains/DomainAndUrlsTests.cs | 2 + .../ContentPickerValueConverterTests.cs | 2 +- .../MarkdownEditorValueConverterTests.cs | 3 +- .../OutputExpansionStrategyTestBase.cs | 2 +- .../Templates/HtmlLocalLinkParserTests.cs | 13 +- umbraco.sln | 8 + 102 files changed, 5881 insertions(+), 132 deletions(-) create mode 100644 src/Umbraco.Core/Models/CacheSettings.cs create mode 100644 src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs create mode 100644 src/Umbraco.Core/PublishedCache/ICacheManager.cs create mode 100644 src/Umbraco.Core/PublishedCache/IDomainCacheService.cs create mode 100644 src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/CacheManager.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ContentData.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ContentNode.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/DomainCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ElementsDictionaryAppCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Factories/ICacheNodeFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/MediaCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/MemberCache.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Persistence/ContentSourceDto.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/PropertyData.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataModel.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializationResult.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializerEntityType.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializer.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializerFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializer.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializerFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/LazyCompressedString.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializer.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializerFactory.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/IMemberCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Services/MemberCacheService.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj create mode 100644 tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Extensions/ContentEditingBuilderExtensions.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithContentTypeKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantNameBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantPropertiesBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithParentKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithTemplateKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithVariantsBuilder.cs create mode 100644 tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 01e3bf1628..5f2a028c77 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,27 +12,29 @@ - + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + @@ -83,7 +85,7 @@ - + diff --git a/global.json b/global.json index 5db4761d46..a718288b1a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100-preview.5.24307.3", + "version": "9.0.100-preview.7.24407.12", "rollForward": "latestFeature", "allowPrerelease": true } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs index 907e3057af..f77afa7347 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UmbracoBuilder.BackOffice.cs @@ -35,6 +35,7 @@ public static partial class UmbracoBuilderExtensions .AddWebServer() .AddRecurringBackgroundJobs() .AddNuCache() + .AddUmbracoHybridCache() .AddDistributedCache() .AddCoreNotifications() .AddExamine() diff --git a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj index 37ab3611de..9ff61d58fd 100644 --- a/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj +++ b/src/Umbraco.Cms.Api.Management/Umbraco.Cms.Api.Management.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs index a6e46ee2e4..9c5030e553 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/DomainCacheRefresher.cs @@ -17,8 +17,10 @@ public sealed class DomainCacheRefresher : PayloadCacheRefresherBase + : base(appCaches, serializer, eventAggregator, factory) + { _publishedSnapshotService = publishedSnapshotService; + } #region Json diff --git a/src/Umbraco.Core/Constants-Configuration.cs b/src/Umbraco.Core/Constants-Configuration.cs index 459becd320..60b3397eeb 100644 --- a/src/Umbraco.Core/Constants-Configuration.cs +++ b/src/Umbraco.Core/Constants-Configuration.cs @@ -65,6 +65,7 @@ public static partial class Constants public const string ConfigDataTypes = ConfigPrefix + "DataTypes"; public const string ConfigPackageManifests = ConfigPrefix + "PackageManifests"; public const string ConfigWebhook = ConfigPrefix + "Webhook"; + public const string ConfigCache = ConfigPrefix + "Cache"; public static class NamedOptions { diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs index 6832bbe789..6c771f5023 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Configuration.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Configuration.Models.Validation; -using Umbraco.Extensions; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.DependencyInjection; @@ -85,7 +85,8 @@ public static partial class UmbracoBuilderExtensions .AddUmbracoOptions() .AddUmbracoOptions() .AddUmbracoOptions() - .AddUmbracoOptions(); + .AddUmbracoOptions() + .AddUmbracoOptions(); // Configure connection string and ensure it's updated when the configuration changes builder.Services.AddSingleton, ConfigureConnectionStrings>(); diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs new file mode 100644 index 0000000000..dcd7211347 --- /dev/null +++ b/src/Umbraco.Core/Models/CacheSettings.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Configuration.Models; + +namespace Umbraco.Cms.Core.Models; + +[UmbracoOptions(Constants.Configuration.ConfigCache)] +public class CacheSettings +{ + /// + /// Gets or sets a value for the collection of content type ids to always have in the cache. + /// + public List ContentTypeKeys { get; set; } = + new(); +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs new file mode 100644 index 0000000000..9095a4aaa3 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedMember.cs @@ -0,0 +1,22 @@ +namespace Umbraco.Cms.Core.Models.PublishedContent; + +public interface IPublishedMember : IPublishedContent +{ + public string Email { get; } + + public string UserName { get; } + + public string? Comments { get; } + + public bool IsApproved { get; } + + public bool IsLockedOut { get; } + + public DateTime? LastLockoutDate { get; } + + public DateTime CreationDate { get; } + + public DateTime? LastLoginDate { get; } + + public DateTime? LastPasswordChangedDate { get; } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs index cefb51241e..11cb52a57d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedMemberCache.cs @@ -10,7 +10,7 @@ public interface IPublishedMemberCache /// /// /// - IPublishedContent? Get(IMember member); + IPublishedMember? Get(IMember member); /// /// Gets a content type identified by its unique identifier. @@ -26,4 +26,12 @@ public interface IPublishedMemberCache /// The content type, or null. /// The alias is case-insensitive. IPublishedContentType GetContentType(string alias); + + /// + /// Get an from an + /// + /// The key of the member to fetch + /// Will fetch draft if this is set to true + /// + Task GetAsync(IMember member); } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs index 398e855343..1c3818b592 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs @@ -196,7 +196,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent var deliveryApiPropertyValueConverter = _converter as IDeliveryApiPropertyValueConverter; - _cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Snapshot; + _cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Elements; _deliveryApiCacheLevel = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevel(this) ?? _cacheLevel; _deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevelForExpansion(this) ?? _cacheLevel; _modelClrType = _converter?.GetPropertyValueType(this) ?? typeof(object); diff --git a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs index 34d9956e88..1c5d240c8f 100644 --- a/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/ContentPickerPropertyEditor.cs @@ -67,7 +67,7 @@ public class ContentPickerPropertyEditor : DataEditor // starting in v14 the passed in value is always a guid, we store it as a document Udi string. Else it's an invalid value public override object? FromEditor(ContentPropertyData editorValue, object? currentValue) => editorValue.Value is not null - && Guid.TryParse(editorValue.Value as string, out Guid guidValue) + && Guid.TryParse(editorValue.Value.ToString(), out Guid guidValue) ? GuidUdi.Create(Constants.UdiEntityType.Document, guidValue).ToString() : null; diff --git a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs index c835c0ae95..82e246feb6 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyCacheLevel.cs @@ -30,6 +30,7 @@ public enum PropertyCacheLevel /// In most cases, a snapshot is created per request, and therefore this is /// equivalent to cache the value for the duration of the request. /// + [Obsolete("Caching no longer supports snapshotting")] Snapshot = 3, /// diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs index 470a95e54e..972f7af03d 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ContentPickerValueConverter.cs @@ -17,14 +17,14 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture), }; - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedContentCache _publishedContentCache; private readonly IApiContentBuilder _apiContentBuilder; public ContentPickerValueConverter( - IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedContentCache publishedContentCache, IApiContentBuilder apiContentBuilder) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedContentCache = publishedContentCache; _apiContentBuilder = apiContentBuilder; } @@ -105,10 +105,9 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery PropertiesToExclude.Contains(propertyType.Alias.ToLower(CultureInfo.InvariantCulture))) == false) { IPublishedContent? content; - IPublishedSnapshot publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot(); if (inter is int id) { - content = publishedSnapshot.Content?.GetById(id); + content = _publishedContentCache.GetById(id); if (content != null) { return content; @@ -121,7 +120,7 @@ public class ContentPickerValueConverter : PropertyValueConverterBase, IDelivery return null; } - content = publishedSnapshot.Content?.GetById(udi.Guid); + content = _publishedContentCache.GetById(udi.Guid); if (content != null && content.ContentType.ItemType == PublishedItemType.Content) { return content; diff --git a/src/Umbraco.Core/PublishedCache/ICacheManager.cs b/src/Umbraco.Core/PublishedCache/ICacheManager.cs new file mode 100644 index 0000000000..062a802adb --- /dev/null +++ b/src/Umbraco.Core/PublishedCache/ICacheManager.cs @@ -0,0 +1,37 @@ +using Umbraco.Cms.Core.Cache; + +namespace Umbraco.Cms.Core.PublishedCache; + +public interface ICacheManager +{ + /// + /// Gets the . + /// + IPublishedContentCache Content { get; } + + /// + /// Gets the . + /// + IPublishedMediaCache Media { get; } + + /// + /// Gets the . + /// + IPublishedMemberCache Members { get; } + + /// + /// Gets the . + /// + IDomainCache Domains { get; } + + /// + /// Gets the elements-level cache. + /// + /// + /// + /// The elements-level cache is shared by all snapshots relying on the same elements, + /// ie all snapshots built on top of unchanging content / media / etc. + /// + /// + IAppCache ElementsCache { get; } +} diff --git a/src/Umbraco.Core/PublishedCache/IDomainCacheService.cs b/src/Umbraco.Core/PublishedCache/IDomainCacheService.cs new file mode 100644 index 0000000000..5d856ff349 --- /dev/null +++ b/src/Umbraco.Core/PublishedCache/IDomainCacheService.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Routing; + +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IDomainCacheService +{ + /// + /// Gets all in the current domain cache, including any domains that may be referenced by + /// documents that are no longer published. + /// + /// + /// + IEnumerable GetAll(bool includeWildcards); + + /// + /// Gets all assigned for specified document, even if it is not published. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + IEnumerable GetAssigned(int documentId, bool includeWildcards = false); + + /// + /// Determines whether a document has domains. + /// + /// The document identifier. + /// A value indicating whether to consider wildcard domains. + bool HasAssigned(int documentId, bool includeWildcards = false); + + void Refresh(DomainCacheRefresher.JsonPayload[] payloads); +} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs index 0bf12d8fbb..e4d8a2311c 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedCache.cs @@ -32,6 +32,7 @@ public interface IPublishedCache /// The content Udi identifier. /// The content, or null. /// The value of overrides defaults. + [Obsolete] // FIXME: Remove when replacing nucache IPublishedContent? GetById(bool preview, Udi contentId); /// @@ -56,25 +57,9 @@ public interface IPublishedCache /// The content unique identifier. /// The content, or null. /// Considers published or unpublished content depending on defaults. + [Obsolete] // FIXME: Remove when replacing nucache IPublishedContent? GetById(Udi contentId); - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// A value indicating whether to consider unpublished content. - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// The value of overrides defaults. - bool HasById(bool preview, int contentId); - - /// - /// Gets a value indicating whether the cache contains a specified content. - /// - /// The content unique identifier. - /// A value indicating whether to the cache contains the specified content. - /// Considers published or unpublished content depending on defaults. - bool HasById(int contentId); - /// /// Gets contents at root. /// @@ -82,6 +67,7 @@ public interface IPublishedCache /// A culture. /// The contents. /// The value of overrides defaults. + [Obsolete] // FIXME: Remove when replacing nucache IEnumerable GetAtRoot(bool preview, string? culture = null); /// @@ -90,6 +76,7 @@ public interface IPublishedCache /// A culture. /// The contents. /// Considers published or unpublished content depending on defaults. + [Obsolete] // FIXME: Remove when replacing nucache IEnumerable GetAtRoot(string? culture = null); /// @@ -98,6 +85,7 @@ public interface IPublishedCache /// A value indicating whether to consider unpublished content. /// A value indicating whether the cache contains published content. /// The value of overrides defaults. + [Obsolete] // FIXME: Remove when replacing nucache bool HasContent(bool preview); /// @@ -105,6 +93,7 @@ public interface IPublishedCache /// /// A value indicating whether the cache contains published content. /// Considers published or unpublished content depending on defaults. + [Obsolete] // FIXME: Remove when replacing nucache bool HasContent(); /// @@ -112,6 +101,7 @@ public interface IPublishedCache /// /// The content type unique identifier. /// The content type, or null. + [Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")] IPublishedContentType? GetContentType(int id); /// @@ -120,6 +110,7 @@ public interface IPublishedCache /// The content type alias. /// The content type, or null. /// The alias is case-insensitive. + [Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")] IPublishedContentType? GetContentType(string alias); /// @@ -127,6 +118,7 @@ public interface IPublishedCache /// /// The content type. /// The contents. + [Obsolete] // FIXME: Remove when replacing nucache IEnumerable GetByContentType(IPublishedContentType contentType); /// @@ -134,5 +126,6 @@ public interface IPublishedCache /// /// The content type key. /// The content type, or null. + [Obsolete("Please use the IContentTypeCacheService instead, scheduled for removal in V16")] IPublishedContentType? GetContentType(Guid key); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs index 6d5fa9b4e8..8353225f10 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedContentCache.cs @@ -4,6 +4,25 @@ namespace Umbraco.Cms.Core.PublishedCache; public interface IPublishedContentCache : IPublishedCache { + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// A value indicating whether to consider unpublished content. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + Task GetByIdAsync(int id, bool preview = false); + + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// A value indicating whether to consider unpublished content. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + Task GetByIdAsync(Guid key, bool preview = false); + + // FIXME: All these routing methods needs to be removed, as they are no longer part of the content cache /// /// Gets content identified by a route. /// @@ -24,6 +43,7 @@ public interface IPublishedContentCache : IPublishedCache /// /// The value of overrides defaults. /// + [Obsolete] IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null); /// @@ -45,6 +65,7 @@ public interface IPublishedContentCache : IPublishedCache /// /// Considers published or unpublished content depending on defaults. /// + [Obsolete] IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null); /// @@ -62,6 +83,7 @@ public interface IPublishedContentCache : IPublishedCache /// /// The value of overrides defaults. /// + [Obsolete] string? GetRouteById(bool preview, int contentId, string? culture = null); /// @@ -76,5 +98,6 @@ public interface IPublishedContentCache : IPublishedCache /// for the current route. If a domain is present the string will be prefixed with the domain ID integer, example: /// {domainId}/route-path-of-item /// + [Obsolete] string? GetRouteById(int contentId, string? culture = null); } diff --git a/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs new file mode 100644 index 0000000000..318e7046c1 --- /dev/null +++ b/src/Umbraco.Core/PublishedCache/IPublishedContentTypeCache.cs @@ -0,0 +1,47 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.PublishedCache; + +public interface IPublishedContentTypeCache +{ + /// + /// Clears the entire cache. + /// + public void ClearAll(); + + /// + /// Clears a cached content type. + /// + /// An identifier. + public void ClearContentType(int id); + + /// + /// Clears all cached content types referencing a data type. + /// + /// A data type identifier. + public void ClearDataType(int id); + + /// + /// Gets a published content type. + /// + /// An item type. + /// An key. + /// The published content type corresponding to the item key. + public IPublishedContentType Get(PublishedItemType itemType, Guid key); + + /// + /// Gets a published content type. + /// + /// An item type. + /// An alias. + /// The published content type corresponding to the item type and alias. + public IPublishedContentType Get(PublishedItemType itemType, string alias); + + /// + /// Gets a published content type. + /// + /// An item type. + /// An identifier. + /// The published content type corresponding to the item type and identifier. + public IPublishedContentType Get(PublishedItemType itemType, int id); +} diff --git a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs index b0fd46748e..eb78109607 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedMediaCache.cs @@ -1,5 +1,22 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + namespace Umbraco.Cms.Core.PublishedCache; public interface IPublishedMediaCache : IPublishedCache { + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + Task GetByIdAsync(int id); + + /// + /// Gets a content identified by its unique identifier. + /// + /// The content unique identifier. + /// The content, or null. + /// Considers published or unpublished content depending on defaults. + Task GetByKeyAsync(Guid key); } diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs index 9987607f62..5b57236d4f 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContentCache.cs @@ -14,6 +14,12 @@ public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublish { } + public Task GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); + + public Task GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException(); + + public Task HasByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); + public IPublishedContent GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); public IPublishedContent GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => @@ -49,4 +55,9 @@ public sealed class InternalPublishedContentCache : PublishedCacheBase, IPublish // public void Add(InternalPublishedContent content) => _content[content.Id] = content.CreateModel(Mock.Of()); public void Clear() => _content.Clear(); + public Task GetByIdAsync(int id) => throw new NotImplementedException(); + + public Task GetByKeyAsync(Guid key) => throw new NotImplementedException(); + + public Task HasByIdAsync(int id) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs index c79506fb5f..dc0fbf281d 100644 --- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs +++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs @@ -23,13 +23,8 @@ public sealed class HtmlLocalLinkParser private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - - public HtmlLocalLinkParser( - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedUrlProvider publishedUrlProvider) + public HtmlLocalLinkParser(IPublishedUrlProvider publishedUrlProvider) { - _umbracoContextAccessor = umbracoContextAccessor; _publishedUrlProvider = publishedUrlProvider; } @@ -50,23 +45,7 @@ public sealed class HtmlLocalLinkParser /// /// /// - public string EnsureInternalLinks(string text, bool preview) - { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } - - if (!preview) - { - return EnsureInternalLinks(text); - } - - using (umbracoContext.ForcedPreview(preview)) // force for URL provider - { - return EnsureInternalLinks(text); - } - } + public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text); /// /// Parses the string looking for the {localLink} syntax and updates them to their correct links. @@ -75,11 +54,6 @@ public sealed class HtmlLocalLinkParser /// public string EnsureInternalLinks(string text) { - if (!_umbracoContextAccessor.TryGetUmbracoContext(out _)) - { - throw new InvalidOperationException("Could not parse internal links, there is no current UmbracoContext"); - } - foreach (LocalLinkTag tagData in FindLocalLinkIds(text)) { if (tagData.Udi is not null) diff --git a/src/Umbraco.Core/Web/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs index 17ffc515a2..7c0bb311bf 100644 --- a/src/Umbraco.Core/Web/IUmbracoContext.cs +++ b/src/Umbraco.Core/Web/IUmbracoContext.cs @@ -31,6 +31,7 @@ public interface IUmbracoContext : IDisposable /// IPublishedSnapshot PublishedSnapshot { get; } + // TODO: Obsolete these, and use cache manager to get /// /// Gets the published content cache. /// diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs index 1dc12a805f..bebf59cbe7 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoDatabaseExtensions.cs @@ -120,6 +120,7 @@ public static partial class NPocoDatabaseExtensions /// once T1 and T2 have completed. Whereas here, it could contain T1's value. /// /// + [Obsolete("Use InsertOrUpdateAsync instead")] public static RecordPersistenceType InsertOrUpdate(this IUmbracoDatabase db, T poco) where T : class => db.InsertOrUpdate(poco, null, null); @@ -150,7 +151,7 @@ public static partial class NPocoDatabaseExtensions /// once T1 and T2 have completed. Whereas here, it could contain T1's value. /// /// - public static RecordPersistenceType InsertOrUpdate( + public static async Task InsertOrUpdateAsync( this IUmbracoDatabase db, T poco, string? updateCommand, @@ -167,7 +168,7 @@ public static partial class NPocoDatabaseExtensions // try to update var rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null - ? db.Update(poco) + ? await db.UpdateAsync(poco) : db.Update(updateCommand!, updateArgs); if (rowCount > 0) { @@ -182,7 +183,7 @@ public static partial class NPocoDatabaseExtensions try { // try to insert - db.Insert(poco); + await db.InsertAsync(poco); return RecordPersistenceType.Insert; } catch (DbException) @@ -193,7 +194,7 @@ public static partial class NPocoDatabaseExtensions // try to update rowCount = updateCommand.IsNullOrWhiteSpace() || updateArgs is null - ? db.Update(poco) + ? await db.UpdateAsync(poco) : db.Update(updateCommand!, updateArgs); if (rowCount > 0) { @@ -209,6 +210,14 @@ public static partial class NPocoDatabaseExtensions throw new DataException("Record could not be inserted or updated."); } + public static RecordPersistenceType InsertOrUpdate( + this IUmbracoDatabase db, + T poco, + string? updateCommand, + object? updateArgs) + where T : class => + db.InsertOrUpdateAsync(poco, updateCommand, updateArgs).GetAwaiter().GetResult(); + /// /// This will escape single @ symbols for npoco values so it doesn't think it's a parameter /// diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index b90ceb9c23..70c41277d0 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -60,6 +60,21 @@ namespace Umbraco.Extensions return sql; } + /// + /// Appends a WHERE IN clause to the Sql statement. + /// + /// The type of the Dto. + /// The Sql statement. + /// An expression specifying the field. + /// The values. + /// The Sql statement. + public static Sql WhereIn(this Sql sql, Expression> field, IEnumerable? values, string alias) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field, alias); + sql.Where(fieldName + " IN (@values)", new { values }); + return sql; + } + /// /// Appends a WHERE IN clause to the Sql statement. /// diff --git a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs index f21635d2df..74f27ba8dd 100644 --- a/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Infrastructure/PublishedCache/PublishedContentTypeCache.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.PublishedCache; /// Represents a content type cache. /// /// This cache is not snapshotted, so it refreshes any time things change. -public class PublishedContentTypeCache : IDisposable +public class PublishedContentTypeCache : IPublishedContentTypeCache { private readonly IContentTypeService? _contentTypeService; private readonly Dictionary _keyToIdMap = new(); @@ -23,11 +23,13 @@ public class PublishedContentTypeCache : IDisposable // NOTE: These are not concurrent dictionaries because all access is done within a lock private readonly Dictionary _typesByAlias = new(); private readonly Dictionary _typesById = new(); - private bool _disposedValue; // default ctor - public PublishedContentTypeCache(IContentTypeService? contentTypeService, IMediaTypeService? mediaTypeService, - IMemberTypeService? memberTypeService, IPublishedContentTypeFactory publishedContentTypeFactory, + public PublishedContentTypeCache( + IContentTypeService? contentTypeService, + IMediaTypeService? mediaTypeService, + IMemberTypeService? memberTypeService, + IPublishedContentTypeFactory publishedContentTypeFactory, ILogger logger) { _contentTypeService = contentTypeService; @@ -48,13 +50,6 @@ public class PublishedContentTypeCache : IDisposable _publishedContentTypeFactory = publishedContentTypeFactory; } - public void Dispose() => - - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose(true); - - // note: cache clearing is performed by XmlStore - /// /// Clears all cached content types. /// @@ -175,7 +170,10 @@ public class PublishedContentTypeCache : IDisposable if (_keyToIdMap.TryGetValue(key, out var id)) { - return Get(itemType, id); + if (_typesById.TryGetValue(id, out IPublishedContentType? foundType)) + { + return foundType; + } } IPublishedContentType type = CreatePublishedContentType(itemType, key); @@ -289,19 +287,6 @@ public class PublishedContentTypeCache : IDisposable } } - protected virtual void Dispose(bool disposing) - { - if (!_disposedValue) - { - if (disposing) - { - _lock.Dispose(); - } - - _disposedValue = true; - } - } - private static string GetAliasKey(PublishedItemType itemType, string alias) { string k; diff --git a/src/Umbraco.PublishedCache.HybridCache/CacheManager.cs b/src/Umbraco.PublishedCache.HybridCache/CacheManager.cs new file mode 100644 index 0000000000..28dfa6ee58 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/CacheManager.cs @@ -0,0 +1,27 @@ +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +/// +public class CacheManager : ICacheManager +{ + public CacheManager(IPublishedContentCache content, IPublishedMediaCache media, IPublishedMemberCache members, IDomainCache domains, IElementsCache elementsCache) + { + ElementsCache = elementsCache; + Content = content; + Media = media; + Members = members; + Domains = domains; + } + + public IPublishedContentCache Content { get; } + + public IPublishedMediaCache Media { get; } + + public IPublishedMemberCache Members { get; } + + public IDomainCache Domains { get; } + + public IAppCache ElementsCache { get; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs b/src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs new file mode 100644 index 0000000000..e72d4f234b --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ContentCacheNode.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects +[ImmutableObject(true)] +internal sealed class ContentCacheNode +{ + public int Id { get; set; } + + public Guid Key { get; set; } + + public int SortOrder { get; set; } + + public DateTime CreateDate { get; set; } + + public int CreatorId { get; set; } + + public int ContentTypeId { get; set; } + + public bool IsDraft { get; set; } + + public ContentData? Data { get; set; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ContentData.cs b/src/Umbraco.PublishedCache.HybridCache/ContentData.cs new file mode 100644 index 0000000000..c314241479 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ContentData.cs @@ -0,0 +1,45 @@ +using System.ComponentModel; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +/// +/// Represents everything that is specific to an edited or published content version +/// +// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects +[ImmutableObject(true)] +internal sealed class ContentData +{ + public ContentData(string? name, string? urlSegment, int versionId, DateTime versionDate, int writerId, int? templateId, bool published, Dictionary? properties, IReadOnlyDictionary? cultureInfos) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + UrlSegment = urlSegment; + VersionId = versionId; + VersionDate = versionDate; + WriterId = writerId; + TemplateId = templateId; + Published = published; + Properties = properties ?? throw new ArgumentNullException(nameof(properties)); + CultureInfos = cultureInfos; + } + + public string Name { get; } + + public string? UrlSegment { get; } + + public int VersionId { get; } + + public DateTime VersionDate { get; } + + public int WriterId { get; } + + public int? TemplateId { get; } + + public bool Published { get; } + + public Dictionary Properties { get; } + + /// + /// The collection of language Id to name for the content item + /// + public IReadOnlyDictionary? CultureInfos { get; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ContentNode.cs b/src/Umbraco.PublishedCache.HybridCache/ContentNode.cs new file mode 100644 index 0000000000..7db0b284ba --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ContentNode.cs @@ -0,0 +1,61 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +// represents a content "node" ie a pair of draft + published versions +// internal, never exposed, to be accessed from ContentStore (only!) +internal sealed class ContentNode +{ + // everything that is common to both draft and published versions + // keep this as small as possible +#pragma warning disable IDE1006 // Naming Styles + public readonly int Id; + + + // draft and published version (either can be null, but not both) + // are models not direct PublishedContent instances + private ContentData? _draftData; + private ContentData? _publishedData; + + public ContentNode( + int id, + Guid key, + int sortOrder, + DateTime createDate, + int creatorId, + IPublishedContentType contentType, + ContentData? draftData, + ContentData? publishedData) + { + Id = id; + Key = key; + SortOrder = sortOrder; + CreateDate = createDate; + CreatorId = creatorId; + ContentType = contentType; + + if (draftData == null && publishedData == null) + { + throw new ArgumentException("Both draftData and publishedData cannot be null at the same time."); + } + + _draftData = draftData; + _publishedData = publishedData; + } + + public bool HasPublished => _publishedData != null; + + public ContentData? DraftModel => _draftData; + + public ContentData? PublishedModel => _publishedData; + + public readonly Guid Key; + public IPublishedContentType ContentType; + public readonly int SortOrder; + public readonly DateTime CreateDate; + public readonly int CreatorId; + + public bool HasPublishedCulture(string culture) => _publishedData != null && (_publishedData.CultureInfos?.ContainsKey(culture) ?? false); +#pragma warning restore IDE1006 // Naming Styles +} diff --git a/src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs b/src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs new file mode 100644 index 0000000000..e8d74daa02 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/CultureVariation.cs @@ -0,0 +1,29 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +/// +/// Represents the culture variation information on a content item +/// +[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys +public class CultureVariation +{ + [DataMember(Order = 0)] + [JsonPropertyName("nm")] + public string? Name { get; set; } + + [DataMember(Order = 1)] + [JsonPropertyName("us")] + public string? UrlSegment { get; set; } + + [DataMember(Order = 2)] + [JsonPropertyName("dt")] + [JsonConverter(typeof(JsonUniversalDateTimeConverter))] + public DateTime Date { get; set; } + + [DataMember(Order = 3)] + [JsonPropertyName("isd")] + public bool IsDraft { get; set; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs new file mode 100644 index 0000000000..6ad695c154 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -0,0 +1,67 @@ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.Serialization; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + + +namespace Umbraco.Extensions; + +/// +/// Extension methods for for the Umbraco's NuCache +/// +public static class UmbracoBuilderExtensions +{ + /// + /// Adds Umbraco NuCache dependencies + /// + public static IUmbracoBuilder AddUmbracoHybridCache(this IUmbracoBuilder builder) + { + builder.Services.AddHybridCache(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(s => + { + IOptions options = s.GetRequiredService>(); + switch (options.Value.NuCacheSerializerType) + { + case NuCacheSerializerType.JSON: + return new JsonContentNestedDataSerializerFactory(); + case NuCacheSerializerType.MessagePack: + return ActivatorUtilities.CreateInstance(s); + default: + throw new IndexOutOfRangeException(); + } + }); + builder.Services.AddSingleton(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); + return builder; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs new file mode 100644 index 0000000000..2723a281c2 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/DocumentCache.cs @@ -0,0 +1,64 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public sealed class DocumentCache : IPublishedContentCache +{ + private readonly IDocumentCacheService _documentCacheService; + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + + public DocumentCache(IDocumentCacheService documentCacheService, IPublishedContentTypeCache publishedContentTypeCache) + { + _documentCacheService = documentCacheService; + _publishedContentTypeCache = publishedContentTypeCache; + } + + public async Task GetByIdAsync(int id, bool preview = false) => await _documentCacheService.GetByIdAsync(id, preview); + + + public async Task GetByIdAsync(Guid key, bool preview = false) => await _documentCacheService.GetByKeyAsync(key, preview); + + public IPublishedContent? GetById(bool preview, int contentId) => GetByIdAsync(contentId, preview).GetAwaiter().GetResult(); + + public IPublishedContent? GetById(bool preview, Guid contentId) => GetByIdAsync(contentId, preview).GetAwaiter().GetResult(); + + + public IPublishedContent? GetById(int contentId) => GetByIdAsync(contentId, false).GetAwaiter().GetResult(); + + public IPublishedContent? GetById(Guid contentId) => GetByIdAsync(contentId, false).GetAwaiter().GetResult(); + + public IPublishedContentType? GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Content, id); + + public IPublishedContentType? GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Content, alias); + + + public IPublishedContentType? GetContentType(Guid key) => _publishedContentTypeCache.Get(PublishedItemType.Content, key); + + // FIXME: These need to be refactored when removing nucache + // Thats the time where we can change the IPublishedContentCache interface. + + public IPublishedContent? GetById(bool preview, Udi contentId) => throw new NotImplementedException(); + + public IPublishedContent? GetById(Udi contentId) => throw new NotImplementedException(); + + public IEnumerable GetAtRoot(bool preview, string? culture = null) => throw new NotImplementedException(); + + public IEnumerable GetAtRoot(string? culture = null) => throw new NotImplementedException(); + + public bool HasContent(bool preview) => throw new NotImplementedException(); + + public bool HasContent() => throw new NotImplementedException(); + + public IEnumerable GetByContentType(IPublishedContentType contentType) => throw new NotImplementedException(); + + public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + + public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => throw new NotImplementedException(); + + public string? GetRouteById(bool preview, int contentId, string? culture = null) => throw new NotImplementedException(); + + public string? GetRouteById(int contentId, string? culture = null) => throw new NotImplementedException(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/DomainCache.cs b/src/Umbraco.PublishedCache.HybridCache/DomainCache.cs new file mode 100644 index 0000000000..8d07ef7dd7 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/DomainCache.cs @@ -0,0 +1,34 @@ +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +/// +/// Implements for NuCache. +/// +public class DomainCache : IDomainCache +{ + private readonly IDomainCacheService _domainCacheService; + + /// + /// Initializes a new instance of the class. + /// + public DomainCache(IDefaultCultureAccessor defaultCultureAccessor, IDomainCacheService domainCacheService) + { + _domainCacheService = domainCacheService; + DefaultCulture = defaultCultureAccessor.DefaultCulture; + } + + /// + public string DefaultCulture { get; } + + /// + public IEnumerable GetAll(bool includeWildcards) => _domainCacheService.GetAll(includeWildcards); + + /// + public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) => _domainCacheService.GetAssigned(documentId, includeWildcards); + + /// + public bool HasAssigned(int documentId, bool includeWildcards = false) => _domainCacheService.HasAssigned(documentId, includeWildcards); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ElementsDictionaryAppCache.cs b/src/Umbraco.PublishedCache.HybridCache/ElementsDictionaryAppCache.cs new file mode 100644 index 0000000000..6415629b38 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ElementsDictionaryAppCache.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Cache; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public class ElementsDictionaryAppCache : FastDictionaryAppCache, IElementsCache +{ +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs new file mode 100644 index 0000000000..7fd91c4603 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs @@ -0,0 +1,120 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Factories; + +internal class CacheNodeFactory : ICacheNodeFactory +{ + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + + public CacheNodeFactory(IShortStringHelper shortStringHelper, UrlSegmentProviderCollection urlSegmentProviders) + { + _shortStringHelper = shortStringHelper; + _urlSegmentProviders = urlSegmentProviders; + } + + public ContentCacheNode ToContentCacheNode(IContent content, bool preview) + { + ContentData contentData = GetContentData(content, !preview, preview ? content.PublishTemplateId : content.TemplateId); + return new ContentCacheNode + { + Id = content.Id, + Key = content.Key, + SortOrder = content.SortOrder, + CreateDate = content.CreateDate, + CreatorId = content.CreatorId, + ContentTypeId = content.ContentTypeId, + Data = contentData, + IsDraft = preview, + }; + } + + public ContentCacheNode ToContentCacheNode(IMedia media) + { + ContentData contentData = GetContentData(media, false, null); + return new ContentCacheNode + { + Id = media.Id, + Key = media.Key, + SortOrder = media.SortOrder, + CreateDate = media.CreateDate, + CreatorId = media.CreatorId, + ContentTypeId = media.ContentTypeId, + Data = contentData, + IsDraft = false, + }; + } + + private ContentData GetContentData(IContentBase content, bool published, int? templateId) + { + var propertyData = new Dictionary(); + foreach (IProperty prop in content.Properties) + { + var pdatas = new List(); + foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) + { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + { + continue; + } + + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value != null) + { + pdatas.Add(new PropertyData + { + Culture = pvalue.Culture ?? string.Empty, + Segment = pvalue.Segment ?? string.Empty, + Value = value, + }); + } + } + + propertyData[prop.Alias] = pdatas.ToArray(); + } + + var cultureData = new Dictionary(); + + // sanitize - names should be ok but ... never knows + if (content.ContentType.VariesByCulture()) + { + ContentCultureInfosCollection? infos = content is IContent document + ? published + ? document.PublishCultureInfos + : document.CultureInfos + : content.CultureInfos; + + // ReSharper disable once UseDeconstruction + if (infos is not null) + { + foreach (ContentCultureInfos cultureInfo in infos) + { + var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); + cultureData[cultureInfo.Culture] = new CultureVariation + { + Name = cultureInfo.Name, + UrlSegment = + content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture), + Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, + IsDraft = cultureIsDraft, + }; + } + } + } + + return new ContentData( + content.Name, + null, + content.VersionId, + content.UpdateDate, + content.CreatorId, + templateId, + published, + propertyData, + cultureData); + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/ICacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/ICacheNodeFactory.cs new file mode 100644 index 0000000000..f16ea2b162 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/ICacheNodeFactory.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Factories; + +internal interface ICacheNodeFactory +{ + ContentCacheNode ToContentCacheNode(IContent content, bool preview); + ContentCacheNode ToContentCacheNode(IMedia media); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs new file mode 100644 index 0000000000..c5bfe4fe9e --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/IPublishedContentFactory.cs @@ -0,0 +1,12 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Factories; + +internal interface IPublishedContentFactory +{ + IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview); + IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode); + + IPublishedMember ToPublishedMember(IMember member); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs new file mode 100644 index 0000000000..1afc363555 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs @@ -0,0 +1,159 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Factories; + +internal class PublishedContentFactory : IPublishedContentFactory +{ + private readonly IElementsCache _elementsCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + + + public PublishedContentFactory( + IElementsCache elementsCache, + IVariationContextAccessor variationContextAccessor, + IPublishedContentTypeCache publishedContentTypeCache) + { + _elementsCache = elementsCache; + _variationContextAccessor = variationContextAccessor; + _publishedContentTypeCache = publishedContentTypeCache; + } + + public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview) + { + IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Content, contentCacheNode.ContentTypeId); + var contentNode = new ContentNode( + contentCacheNode.Id, + contentCacheNode.Key, + contentCacheNode.SortOrder, + contentCacheNode.CreateDate, + contentCacheNode.CreatorId, + contentType, + preview ? contentCacheNode.Data : null, + preview ? null : contentCacheNode.Data); + + IPublishedContent? model = GetModel(contentNode, preview); + + if (preview) + { + return model ?? GetPublishedContentAsDraft(model); + } + + return model; + } + + public IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode) + { + IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Media, contentCacheNode.ContentTypeId); + var contentNode = new ContentNode( + contentCacheNode.Id, + contentCacheNode.Key, + contentCacheNode.SortOrder, + contentCacheNode.CreateDate, + contentCacheNode.CreatorId, + contentType, + null, + contentCacheNode.Data); + + return GetModel(contentNode, false); + } + + public IPublishedMember ToPublishedMember(IMember member) + { + IPublishedContentType contentType = _publishedContentTypeCache.Get(PublishedItemType.Member, member.ContentTypeId); + + // Members are only "mapped" never cached, so these default values are a bit wierd, but they are not used. + var contentData = new ContentData( + member.Name, + null, + 0, + member.UpdateDate, + member.CreatorId, + null, + true, + GetPropertyValues(contentType, member), + null); + + var contentNode = new ContentNode( + member.Id, + member.Key, + member.SortOrder, + member.UpdateDate, + member.CreatorId, + contentType, + null, + contentData); + return new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor); + } + + private Dictionary GetPropertyValues(IPublishedContentType contentType, IMember member) + { + var properties = member + .Properties + .ToDictionary( + x => x.Alias, + x => new[] { new PropertyData { Value = x.GetValue(), Culture = string.Empty, Segment = string.Empty } }, + StringComparer.OrdinalIgnoreCase); + + // Add member properties + AddIf(contentType, properties, nameof(IMember.Email), member.Email); + AddIf(contentType, properties, nameof(IMember.Username), member.Username); + AddIf(contentType, properties, nameof(IMember.Comments), member.Comments); + AddIf(contentType, properties, nameof(IMember.IsApproved), member.IsApproved); + AddIf(contentType, properties, nameof(IMember.IsLockedOut), member.IsLockedOut); + AddIf(contentType, properties, nameof(IMember.LastLockoutDate), member.LastLockoutDate); + AddIf(contentType, properties, nameof(IMember.CreateDate), member.CreateDate); + AddIf(contentType, properties, nameof(IMember.LastLoginDate), member.LastLoginDate); + AddIf(contentType, properties, nameof(IMember.LastPasswordChangeDate), member.LastPasswordChangeDate); + + return properties; + } + + private void AddIf(IPublishedContentType contentType, IDictionary properties, string alias, object? value) + { + IPublishedPropertyType? propertyType = contentType.GetPropertyType(alias); + if (propertyType == null || propertyType.IsUserProperty) + { + return; + } + + properties[alias] = new[] { new PropertyData { Value = value, Culture = string.Empty, Segment = string.Empty } }; + } + + private IPublishedContent? GetModel(ContentNode node, bool preview) + { + ContentData? contentData = preview ? node.DraftModel : node.PublishedModel; + return contentData == null + ? null + : new PublishedContent( + node, + preview, + _elementsCache, + _variationContextAccessor); + } + + + private IPublishedContent? GetPublishedContentAsDraft(IPublishedContent? content) => + content == null ? null : + // an object in the cache is either an IPublishedContentOrMedia, + // or a model inheriting from PublishedContentExtended - in which + // case we need to unwrap to get to the original IPublishedContentOrMedia. + UnwrapIPublishedContent(content); + + private PublishedContent UnwrapIPublishedContent(IPublishedContent content) + { + while (content is PublishedContentWrapped wrapped) + { + content = wrapped.Unwrap(); + } + + if (!(content is PublishedContent inner)) + { + throw new InvalidOperationException("Innermost content is not PublishedContent."); + } + + return inner; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs b/src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs new file mode 100644 index 0000000000..873a128d53 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/IElementsCache.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Cache; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface IElementsCache : IAppCache +{ +} diff --git a/src/Umbraco.PublishedCache.HybridCache/MediaCache.cs b/src/Umbraco.PublishedCache.HybridCache/MediaCache.cs new file mode 100644 index 0000000000..53d59da72c --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/MediaCache.cs @@ -0,0 +1,56 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public class MediaCache : IPublishedMediaCache +{ + private readonly IMediaCacheService _mediaCacheService; + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + + public MediaCache(IMediaCacheService mediaCacheService, IPublishedContentTypeCache publishedContentTypeCache) + { + _mediaCacheService = mediaCacheService; + _publishedContentTypeCache = publishedContentTypeCache; + } + + public async Task GetByIdAsync(int id) => await _mediaCacheService.GetByIdAsync(id); + + public async Task GetByKeyAsync(Guid key) => await _mediaCacheService.GetByKeyAsync(key); + + public IPublishedContent? GetById(bool preview, int contentId) => GetByIdAsync(contentId).GetAwaiter().GetResult(); + + public IPublishedContent? GetById(bool preview, Guid contentId) => + GetByKeyAsync(contentId).GetAwaiter().GetResult(); + + + public IPublishedContent? GetById(int contentId) => GetByIdAsync(contentId).GetAwaiter().GetResult(); + + public IPublishedContent? GetById(Guid contentId) => GetByKeyAsync(contentId).GetAwaiter().GetResult(); + + + public IPublishedContentType? GetContentType(Guid key) => _publishedContentTypeCache.Get(PublishedItemType.Media, key); + + public IPublishedContentType GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Media, id); + + public IPublishedContentType GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Media, alias); + + // FIXME - these need to be removed when removing nucache + public IPublishedContent? GetById(bool preview, Udi contentId) => throw new NotImplementedException(); + + public IPublishedContent? GetById(Udi contentId) => throw new NotImplementedException(); + + public IEnumerable GetAtRoot(bool preview, string? culture = null) => throw new NotImplementedException(); + + public IEnumerable GetAtRoot(string? culture = null) => throw new NotImplementedException(); + + public bool HasContent(bool preview) => throw new NotImplementedException(); + + public bool HasContent() => throw new NotImplementedException(); + + + public IEnumerable GetByContentType(IPublishedContentType contentType) => + throw new NotImplementedException(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/MemberCache.cs b/src/Umbraco.PublishedCache.HybridCache/MemberCache.cs new file mode 100644 index 0000000000..e5029d16e6 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/MemberCache.cs @@ -0,0 +1,27 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public class MemberCache : IPublishedMemberCache +{ + private readonly IMemberCacheService _memberCacheService; + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + + public MemberCache(IMemberCacheService memberCacheService, IPublishedContentTypeCache publishedContentTypeCache) + { + _memberCacheService = memberCacheService; + _publishedContentTypeCache = publishedContentTypeCache; + } + + public async Task GetAsync(IMember member) => + await _memberCacheService.Get(member); + + public IPublishedMember? Get(IMember member) => GetAsync(member).GetAwaiter().GetResult(); + + public IPublishedContentType GetContentType(int id) => _publishedContentTypeCache.Get(PublishedItemType.Member, id); + + public IPublishedContentType GetContentType(string alias) => _publishedContentTypeCache.Get(PublishedItemType.Member, alias); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs new file mode 100644 index 0000000000..105fad1d9d --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs @@ -0,0 +1,124 @@ +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Infrastructure.HybridCache.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; + +internal sealed class CacheRefreshingNotificationHandler : + INotificationAsyncHandler, + INotificationAsyncHandler, + INotificationAsyncHandler, + INotificationAsyncHandler, + INotificationAsyncHandler +{ + private readonly IDocumentCacheService _documentCacheService; + private readonly IMediaCacheService _mediaCacheService; + private readonly IElementsCache _elementsCache; + private readonly IRelationService _relationService; + private readonly IPublishedContentTypeCache _publishedContentTypeCache; + + public CacheRefreshingNotificationHandler( + IDocumentCacheService documentCacheService, + IMediaCacheService mediaCacheService, + IElementsCache elementsCache, + IRelationService relationService, + IPublishedContentTypeCache publishedContentTypeCache) + { + _documentCacheService = documentCacheService; + _mediaCacheService = mediaCacheService; + _elementsCache = elementsCache; + _relationService = relationService; + _publishedContentTypeCache = publishedContentTypeCache; + } + + public async Task HandleAsync(ContentRefreshNotification notification, CancellationToken cancellationToken) + { + await RefreshElementsCacheAsync(notification.Entity); + + await _documentCacheService.RefreshContentAsync(notification.Entity); + } + + public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) + { + foreach (IContent deletedEntity in notification.DeletedEntities) + { + await RefreshElementsCacheAsync(deletedEntity); + await _documentCacheService.DeleteItemAsync(deletedEntity.Id); + } + } + + public async Task HandleAsync(MediaRefreshNotification notification, CancellationToken cancellationToken) + { + await RefreshElementsCacheAsync(notification.Entity); + await _mediaCacheService.RefreshMediaAsync(notification.Entity); + } + + public async Task HandleAsync(MediaDeletedNotification notification, CancellationToken cancellationToken) + { + foreach (IMedia deletedEntity in notification.DeletedEntities) + { + await RefreshElementsCacheAsync(deletedEntity); + await _mediaCacheService.DeleteItemAsync(deletedEntity.Id); + } + } + + private async Task RefreshElementsCacheAsync(IUmbracoEntity content) + { + IEnumerable parentRelations = _relationService.GetByParent(content)!; + IEnumerable childRelations = _relationService.GetByChild(content); + + var ids = parentRelations.Select(x => x.ChildId).Concat(childRelations.Select(x => x.ParentId)).ToHashSet(); + foreach (var id in ids) + { + if (await _documentCacheService.HasContentByIdAsync(id) is false) + { + continue; + } + + IPublishedContent? publishedContent = await _documentCacheService.GetByIdAsync(id); + if (publishedContent is null) + { + continue; + } + + foreach (IPublishedProperty publishedProperty in publishedContent.Properties) + { + var property = (PublishedProperty) publishedProperty; + if (property.ReferenceCacheLevel != PropertyCacheLevel.Elements) + { + continue; + } + + _elementsCache.ClearByKey(property.ValuesCacheKey); + } + } + } + + public Task HandleAsync(ContentTypeRefreshedNotification notification, CancellationToken cancellationToken) + { + const ContentTypeChangeTypes types // only for those that have been refreshed + = ContentTypeChangeTypes.RefreshMain | ContentTypeChangeTypes.RefreshOther | ContentTypeChangeTypes.Remove; + var contentTypeIds = notification.Changes.Where(x => x.ChangeTypes.HasTypesAny(types)).Select(x => x.Item.Id) + .ToArray(); + + if (contentTypeIds.Length != 0) + { + foreach (var contentTypeId in contentTypeIds) + { + _publishedContentTypeCache.ClearContentType(contentTypeId); + } + + _documentCacheService.Rebuild(contentTypeIds); + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs new file mode 100644 index 0000000000..d0dfa76b67 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Infrastructure.HybridCache.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; + +internal class SeedingNotificationHandler : INotificationAsyncHandler +{ + private readonly IDocumentCacheService _documentCacheService; + private readonly CacheSettings _cacheSettings; + + public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IOptions cacheSettings) + { + _documentCacheService = documentCacheService; + _cacheSettings = cacheSettings.Value; + } + + public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken) => await _documentCacheService.SeedAsync(_cacheSettings.ContentTypeKeys); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/ContentSourceDto.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/ContentSourceDto.cs new file mode 100644 index 0000000000..4d4fcae73d --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/ContentSourceDto.cs @@ -0,0 +1,67 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence +{ + // read-only dto + internal class ContentSourceDto : IReadOnlyContentBase + { + public int Id { get; init; } + + public Guid Key { get; init; } + + public int ContentTypeId { get; init; } + + public int Level { get; init; } + + public string Path { get; init; } = string.Empty; + + public int SortOrder { get; init; } + + public int ParentId { get; init; } + + public bool Published { get; init; } + + public bool Edited { get; init; } + + public DateTime CreateDate { get; init; } + + public int CreatorId { get; init; } + + // edited data + public int VersionId { get; init; } + + public string? EditName { get; init; } + + public DateTime EditVersionDate { get; init; } + + public int EditWriterId { get; init; } + + public int EditTemplateId { get; init; } + + public string? EditData { get; init; } + + public byte[]? EditDataRaw { get; init; } + + // published data + public int PublishedVersionId { get; init; } + + public string? PubName { get; init; } + + public DateTime PubVersionDate { get; init; } + + public int PubWriterId { get; init; } + + public int PubTemplateId { get; init; } + + public string? PubData { get; init; } + + public byte[]? PubDataRaw { get; init; } + + // Explicit implementation + DateTime IReadOnlyContentBase.UpdateDate => EditVersionDate; + + string? IReadOnlyContentBase.Name => EditName; + + int IReadOnlyContentBase.WriterId => EditWriterId; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs new file mode 100644 index 0000000000..d49d2f8799 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs @@ -0,0 +1,895 @@ +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Querying; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Infrastructure.HybridCache.Serialization; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; +using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRepository +{ + private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory; + private readonly IDocumentRepository _documentRepository; + private readonly ILogger _logger; + private readonly IMediaRepository _mediaRepository; + private readonly IMemberRepository _memberRepository; + private readonly IOptions _nucacheSettings; + private readonly IShortStringHelper _shortStringHelper; + private readonly UrlSegmentProviderCollection _urlSegmentProviders; + + /// + /// Initializes a new instance of the class. + /// + public DatabaseCacheRepository( + IScopeAccessor scopeAccessor, + AppCaches appCaches, + ILogger logger, + IMemberRepository memberRepository, + IDocumentRepository documentRepository, + IMediaRepository mediaRepository, + IShortStringHelper shortStringHelper, + UrlSegmentProviderCollection urlSegmentProviders, + IContentCacheDataSerializerFactory contentCacheDataSerializerFactory, + IOptions nucacheSettings) + : base(scopeAccessor, appCaches) + { + _logger = logger; + _memberRepository = memberRepository; + _documentRepository = documentRepository; + _mediaRepository = mediaRepository; + _shortStringHelper = shortStringHelper; + _urlSegmentProviders = urlSegmentProviders; + _contentCacheDataSerializerFactory = contentCacheDataSerializerFactory; + _nucacheSettings = nucacheSettings; + } + + public async Task DeleteContentItemAsync(int id) + => await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = id }); + + public async Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState) + { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + + // always refresh the edited data + await OnRepositoryRefreshed(serializer, contentCacheNode, true); + + switch (publishedState) + { + case PublishedState.Publishing: + await OnRepositoryRefreshed(serializer, contentCacheNode, false); + break; + case PublishedState.Unpublishing: + await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = contentCacheNode.Id }); + break; + } + } + + public async Task RefreshMediaAsync(ContentCacheNode contentCacheNode) + { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + await OnRepositoryRefreshed(serializer, contentCacheNode, false); + } + + /// + public void Rebuild( + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null) + { + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create( + ContentCacheDataSerializerEntityType.Document + | ContentCacheDataSerializerEntityType.Media + | ContentCacheDataSerializerEntityType.Member); + + // If contentTypeIds, mediaTypeIds and memberTypeIds are null, truncate table as all records will be deleted (as these 3 are the only types in the table). + if (contentTypeIds != null && !contentTypeIds.Any() + && mediaTypeIds != null && !mediaTypeIds.Any() + && memberTypeIds != null && !memberTypeIds.Any()) + { + if (Database.DatabaseType == DatabaseType.SqlServer2012) + { + Database.Execute($"TRUNCATE TABLE cmsContentNu"); + } + + if (Database.DatabaseType == DatabaseType.SQLite) + { + Database.Execute($"DELETE FROM cmsContentNu"); + } + } + + if (contentTypeIds != null) + { + RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds); + } + + if (mediaTypeIds != null) + { + RebuildMediaDbCache(serializer, _nucacheSettings.Value.SqlPageSize, mediaTypeIds); + } + + if (memberTypeIds != null) + { + RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds); + } + } + + // assumes content tree lock + public bool VerifyContentDbCache() + { + // every document should have a corresponding row for edited properties + // and if published, may have a corresponding row for published properties + Guid contentObjectType = Constants.ObjectTypes.Document; + + var count = Database.ExecuteScalar( + $@"SELECT COUNT(*) +FROM umbracoNode +JOIN {Constants.DatabaseSchema.Tables.Document} ON umbracoNode.id={Constants.DatabaseSchema.Tables.Document}.nodeId +LEFT JOIN cmsContentNu nuEdited ON (umbracoNode.id=nuEdited.nodeId AND nuEdited.published=0) +LEFT JOIN cmsContentNu nuPublished ON (umbracoNode.id=nuPublished.nodeId AND nuPublished.published=1) +WHERE umbracoNode.nodeObjectType=@objType +AND nuEdited.nodeId IS NULL OR ({Constants.DatabaseSchema.Tables.Document}.published=1 AND nuPublished.nodeId IS NULL);", + new { objType = contentObjectType }); + + return count == 0; + } + + // assumes media tree lock + public bool VerifyMediaDbCache() + { + // every media item should have a corresponding row for edited properties + Guid mediaObjectType = Constants.ObjectTypes.Media; + + var count = Database.ExecuteScalar( + @"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentNu.nodeId IS NULL +", + new { objType = mediaObjectType }); + + return count == 0; + } + + // assumes member tree lock + public bool VerifyMemberDbCache() + { + // every member item should have a corresponding row for edited properties + Guid memberObjectType = Constants.ObjectTypes.Member; + + var count = Database.ExecuteScalar( + @"SELECT COUNT(*) +FROM umbracoNode +LEFT JOIN cmsContentNu ON (umbracoNode.id=cmsContentNu.nodeId AND cmsContentNu.published=0) +WHERE umbracoNode.nodeObjectType=@objType +AND cmsContentNu.nodeId IS NULL +", + new { objType = memberObjectType }); + + return count == 0; + } + + public async Task GetContentSourceAsync(int id, bool preview = false) + { + Sql? sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeId(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto == null) + { + return null; + } + + if (preview is false && dto.PubDataRaw is null && dto.PubData is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + return CreateContentNodeKit(dto, serializer, preview); + } + + public IEnumerable GetContentByContentTypeKey(IEnumerable keys) + { + if (keys.Any() is false) + { + yield break; + } + + Sql? sql = SqlContentSourcesSelect() + .InnerJoin("n") + .On((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent") + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .WhereIn(x => x.UniqueId, keys,"n") + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + + IEnumerable dtos = GetContentNodeDtos(sql); + + foreach (ContentSourceDto row in dtos) + { + yield return CreateContentNodeKit(row, serializer, row.Published is false); + } + } + + public async Task GetMediaSourceAsync(int id) + { + Sql? sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeId(SqlContext, id)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); + } + + private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview) + { + // use a custom SQL to update row version on each update + // db.InsertOrUpdate(dto); + ContentNuDto dto = GetDtoFromCacheNode(content, !preview, serializer); + + await Database.InsertOrUpdateAsync( + dto, + "SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published", + new + { + dataRaw = dto.RawData ?? Array.Empty(), + data = dto.Data, + id = dto.NodeId, + published = dto.Published, + }); + } + + // assumes content tree lock + private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection? contentTypeIds) + { + Guid contentObjectType = Constants.ObjectTypes.Document; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = contentObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = contentObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + IQuery query = SqlContext.Query(); + if (contentTypeIds != null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // the tree is locked, counting and comparing to total is safe + IEnumerable descendants = + _documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + var items = new List(); + var count = 0; + foreach (IContent c in descendants) + { + // always the edited version + items.Add(GetDtoFromContent(c, false, serializer)); + + // and also the published version if it makes any sense + if (c.Published) + { + items.Add(GetDtoFromContent(c, true, serializer)); + } + + count++; + } + + Database.BulkInsertRecords(items); + processed += count; + } while (processed < total); + } + + // assumes media tree lock + private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, + IReadOnlyCollection? contentTypeIds) + { + Guid mediaObjectType = Constants.ObjectTypes.Media; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds is null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = mediaObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = mediaObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + IQuery query = SqlContext.Query(); + if (contentTypeIds is not null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + // the tree is locked, counting and comparing to total is safe + IEnumerable descendants = + _mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + var items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray(); + Database.BulkInsertRecords(items); + processed += items.Length; + } while (processed < total); + } + + // assumes member tree lock + private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, + IReadOnlyCollection? contentTypeIds) + { + Guid memberObjectType = Constants.ObjectTypes.Member; + + // remove all - if anything fails the transaction will rollback + if (contentTypeIds == null || contentTypeIds.Count == 0) + { + // must support SQL-CE + Database.Execute( + @"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType +)", + new { objType = memberObjectType }); + } + else + { + // assume number of ctypes won't blow IN(...) + // must support SQL-CE + Database.Execute( + $@"DELETE FROM cmsContentNu +WHERE cmsContentNu.nodeId IN ( + SELECT id FROM umbracoNode + JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id + WHERE umbracoNode.nodeObjectType=@objType + AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes) +)", + new { objType = memberObjectType, ctypes = contentTypeIds }); + } + + // insert back - if anything fails the transaction will rollback + IQuery query = SqlContext.Query(); + if (contentTypeIds != null && contentTypeIds.Count > 0) + { + query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...) + } + + long pageIndex = 0; + long processed = 0; + long total; + do + { + IEnumerable descendants = + _memberRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path")); + ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray(); + Database.BulkInsertRecords(items); + processed += items.Length; + } while (processed < total); + } + + private ContentNuDto GetDtoFromCacheNode(ContentCacheNode cacheNode, bool published, IContentCacheDataSerializer serializer) + { + // the dictionary that will be serialized + var contentCacheData = new ContentCacheDataModel + { + PropertyData = cacheNode.Data?.Properties, + CultureData = cacheNode.Data?.CultureInfos?.ToDictionary(), + UrlSegment = cacheNode.Data?.UrlSegment, + }; + + // TODO: We should probably fix all serialization to only take ContentTypeId, for now it takes an IReadOnlyContentBase + // but it is only the content type id that is needed. + ContentCacheDataSerializationResult serialized = serializer.Serialize(new ContentSourceDto { ContentTypeId = cacheNode.ContentTypeId, }, contentCacheData, published); + + var dto = new ContentNuDto + { + NodeId = cacheNode.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData, + }; + + return dto; + } + + private ContentNuDto GetDtoFromContent(IContentBase content, bool published, IContentCacheDataSerializer serializer) + { + // should inject these in ctor + // BUT for the time being we decide not to support ConvertDbToXml/String + // var propertyEditorResolver = PropertyEditorResolver.Current; + // var dataTypeService = ApplicationContext.Current.Services.DataTypeService; + var propertyData = new Dictionary(); + foreach (IProperty prop in content.Properties) + { + var pdatas = new List(); + foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture)) + { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + { + continue; + } + + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' + var value = published ? pvalue.PublishedValue : pvalue.EditedValue; + if (value != null) + { + pdatas.Add(new PropertyData + { + Culture = pvalue.Culture ?? string.Empty, + Segment = pvalue.Segment ?? string.Empty, + Value = value, + }); + } + } + + propertyData[prop.Alias] = pdatas.ToArray(); + } + + var cultureData = new Dictionary(); + + // sanitize - names should be ok but ... never knows + if (content.ContentType.VariesByCulture()) + { + ContentCultureInfosCollection? infos = content is IContent document + ? published + ? document.PublishCultureInfos + : document.CultureInfos + : content.CultureInfos; + + // ReSharper disable once UseDeconstruction + if (infos is not null) + { + foreach (ContentCultureInfos cultureInfo in infos) + { + var cultureIsDraft = !published && content is IContent d && d.IsCultureEdited(cultureInfo.Culture); + cultureData[cultureInfo.Culture] = new CultureVariation + { + Name = cultureInfo.Name, + UrlSegment = + content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders, cultureInfo.Culture), + Date = content.GetUpdateDate(cultureInfo.Culture) ?? DateTime.MinValue, + IsDraft = cultureIsDraft, + }; + } + } + } + + // the dictionary that will be serialized + var contentCacheData = new ContentCacheDataModel + { + PropertyData = propertyData, + CultureData = cultureData, + UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders), + }; + + ContentCacheDataSerializationResult serialized = + serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published); + + var dto = new ContentNuDto + { + NodeId = content.Id, Published = published, Data = serialized.StringData, RawData = serialized.ByteData, + }; + + return dto; + } + + // we want arrays, we want them all loaded, not an enumerable + private Sql SqlContentSourcesSelect(Func>? joins = null) + { + SqlTemplate sqlTemplate = SqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesSelect, + tsql => + tsql.Select( + x => Alias(x.NodeId, "Id"), + x => Alias(x.UniqueId, "Key"), + x => Alias(x.Level, "Level"), + x => Alias(x.Path, "Path"), + x => Alias(x.SortOrder, "SortOrder"), + x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), + x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect(x => Alias(x.Published, "Published"), x => Alias(x.Edited, "Edited")) + .AndSelect( + x => Alias(x.Id, "VersionId"), + x => Alias(x.Text, "EditName"), + x => Alias(x.VersionDate, "EditVersionDate"), + x => Alias(x.UserId, "EditWriterId")) + .AndSelect(x => Alias(x.TemplateId, "EditTemplateId")) + .AndSelect( + "pcver", + x => Alias(x.Id, "PublishedVersionId"), + x => Alias(x.Text, "PubName"), + x => Alias(x.VersionDate, "PubVersionDate"), + x => Alias(x.UserId, "PubWriterId")) + .AndSelect("pdver", x => Alias(x.TemplateId, "PubTemplateId")) + .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .AndSelect("nuPub", x => Alias(x.Data, "PubData")) + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .AndSelect("nuPub", x => Alias(x.RawData, "PubDataRaw")) + .From()); + + Sql? sql = sqlTemplate.Sql(); + + // TODO: I'm unsure how we can format the below into SQL templates also because right.Current and right.Published end up being parameters + if (joins != null) + { + sql = sql.Append(joins(sql.SqlContext)); + } + + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin() + .On((left, right) => left.Id == right.Id) + .LeftJoin( + j => + j.InnerJoin("pdver") + .On( + (left, right) => left.Id == right.Id && right.Published == true, "pcver", "pdver"), + "pcver") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver") + .LeftJoin("nuEdit").On( + (left, right) => left.NodeId == right.NodeId && right.Published == false, aliasRight: "nuEdit") + .LeftJoin("nuPub").On( + (left, right) => left.NodeId == right.NodeId && right.Published == true, aliasRight: "nuPub"); + + return sql; + } + + private Sql SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder => + builder.InnerJoin("x") + .On( + (left, right) => left.NodeId == right.NodeId || + SqlText(left.Path, right.Path, + (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), + aliasRight: "x")); + + Sql sql = sqlTemplate.Sql(); + return sql; + } + + private Sql SqlWhereNodeId(ISqlContext sqlContext, int id) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId, + builder => + builder.Where(x => x.NodeId == SqlTemplate.Arg("id"))); + + Sql sql = sqlTemplate.Sql(id); + return sql; + } + + private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.OrderByLevelIdSortOrder, s => + s.OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder)); + + Sql sql = sqlTemplate.Sql(); + return sql; + } + + private Sql SqlObjectTypeNotTrashed(ISqlContext sqlContext, Guid nodeObjectType) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.ObjectTypeNotTrashedFilter, s => + s.Where(x => + x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && + x.Trashed == SqlTemplate.Arg("trashed"))); + + Sql sql = sqlTemplate.Sql(nodeObjectType, false); + return sql; + } + + /// + /// Returns a slightly more optimized query to use for the document counting when paging over the content sources + /// + /// + /// + private Sql SqlContentSourcesCount(Func>? joins = null) + { + SqlTemplate sqlTemplate = SqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.ContentSourcesCount, tsql => + tsql.Select(x => Alias(x.NodeId, "Id")) + .From() + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin().On((left, right) => left.NodeId == right.NodeId)); + + Sql? sql = sqlTemplate.Sql(); + + if (joins != null) + { + sql = sql.Append(joins(sql.SqlContext)); + } + + // TODO: We can't use a template with this one because of the 'right.Current' and 'right.Published' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .InnerJoin() + .On((left, right) => left.Id == right.Id) + .LeftJoin( + j => + j.InnerJoin("pdver") + .On( + (left, right) => left.Id == right.Id && right.Published, + "pcver", + "pdver"), + "pcver") + .On((left, right) => left.NodeId == right.NodeId, aliasRight: "pcver"); + + return sql; + } + + private Sql SqlMediaSourcesSelect(Func>? joins = null) + { + SqlTemplate sqlTemplate = SqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.MediaSourcesSelect, tsql => + tsql.Select( + x => Alias(x.NodeId, "Id"), + x => Alias(x.UniqueId, "Key"), + x => Alias(x.Level, "Level"), + x => Alias(x.Path, "Path"), + x => Alias(x.SortOrder, "SortOrder"), + x => Alias(x.ParentId, "ParentId"), + x => Alias(x.CreateDate, "CreateDate"), + x => Alias(x.UserId, "CreatorId")) + .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) + .AndSelect( + x => Alias(x.Id, "VersionId"), + x => Alias(x.Text, "EditName"), + x => Alias(x.VersionDate, "EditVersionDate"), + x => Alias(x.UserId, "EditWriterId")) + .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) + .AndSelect("nuEdit", x => Alias(x.RawData, "EditDataRaw")) + .From()); + + Sql? sql = sqlTemplate.Sql(); + + if (joins != null) + { + sql = sql.Append(joins(sql.SqlContext)); + } + + // TODO: We can't use a template with this one because of the 'right.Published' ends up being a parameter so not sure how we can do that + sql = sql + .InnerJoin().On((left, right) => left.NodeId == right.NodeId) + .InnerJoin() + .On((left, right) => left.NodeId == right.NodeId && right.Current) + .LeftJoin("nuEdit") + .On( + (left, right) => left.NodeId == right.NodeId && !right.Published, + aliasRight: "nuEdit"); + + return sql; + } + + private ContentCacheNode CreateContentNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer, bool preview) + { + if (preview) + { + if (dto.EditData == null && dto.EditDataRaw == null) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + + ", consider rebuilding."); + } + + _logger.LogWarning( + "Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", + dto.Id); + } + else + { + bool published = false; + ContentCacheDataModel? deserializedDraftContent = + serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published); + var draftContentData = new ContentData( + dto.EditName, + null, + dto.VersionId, + dto.EditVersionDate, + dto.CreatorId, + dto.EditTemplateId == 0 ? null : dto.EditTemplateId, + published, + deserializedDraftContent?.PropertyData, + deserializedDraftContent?.CultureData); + + return new ContentCacheNode + { + Id = dto.Id, + Key = dto.Key, + SortOrder = dto.SortOrder, + CreateDate = dto.CreateDate, + CreatorId = dto.CreatorId, + ContentTypeId = dto.ContentTypeId, + Data = draftContentData, + IsDraft = true, + }; + } + } + + if (dto.PubData == null && dto.PubDataRaw == null) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + + ", consider rebuilding."); + } + + _logger.LogWarning( + "Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", + dto.Id); + } + + ContentCacheDataModel? deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, true); + var publishedContentData = new ContentData( + dto.PubName, + null, + dto.VersionId, + dto.PubVersionDate, + dto.CreatorId, + dto.EditTemplateId == 0 ? null : dto.EditTemplateId, + true, + deserializedContent?.PropertyData, + deserializedContent?.CultureData); + + return new ContentCacheNode + { + Id = dto.Id, + Key = dto.Key, + SortOrder = dto.SortOrder, + CreateDate = dto.CreateDate, + CreatorId = dto.CreatorId, + ContentTypeId = dto.ContentTypeId, + Data = publishedContentData, + IsDraft = false, + }; + } + + private ContentCacheNode CreateMediaNodeKit(ContentSourceDto dto, IContentCacheDataSerializer serializer) + { + if (dto.EditData == null && dto.EditDataRaw == null) + { + throw new InvalidOperationException("No data for media " + dto.Id); + } + + ContentCacheDataModel? deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, true); + + var publishedContentData = new ContentData( + dto.EditName, + null, + dto.VersionId, + dto.EditVersionDate, + dto.CreatorId, + dto.EditTemplateId == 0 ? null : dto.EditTemplateId, + true, + deserializedMedia?.PropertyData, + deserializedMedia?.CultureData); + + return new ContentCacheNode + { + Id = dto.Id, + Key = dto.Key, + SortOrder = dto.SortOrder, + CreateDate = dto.CreateDate, + CreatorId = dto.CreatorId, + ContentTypeId = dto.ContentTypeId, + Data = publishedContentData, + IsDraft = false, + }; + } + + private IEnumerable GetContentNodeDtos(Sql sql) + { + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + // QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled. + IEnumerable dtos; + if (_nucacheSettings.Value.UsePagedSqlQuery) + { + // Use a more efficient COUNT query + Sql? sqlCountQuery = SqlContentSourcesCount() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)); + + Sql? sqlCount = + SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl"); + + dtos = Database.QueryPaged(_nucacheSettings.Value.SqlPageSize, sql, sqlCount); + } + else + { + dtos = Database.Fetch(sql); + } + + return dtos; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs new file mode 100644 index 0000000000..47c18c07e1 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs @@ -0,0 +1,57 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +internal interface IDatabaseCacheRepository +{ + Task DeleteContentItemAsync(int id); + + Task GetContentSourceAsync(int id, bool preview = false); + + Task GetMediaSourceAsync(int id); + + IEnumerable GetContentByContentTypeKey(IEnumerable keys); + + /// + /// Refreshes the nucache database row for the given cache node /> + /// + /// A representing the asynchronous operation. + Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState); + + /// + /// Refreshes the nucache database row for the given cache node /> + /// + /// A representing the asynchronous operation. + Task RefreshMediaAsync(ContentCacheNode contentCacheNode); + + /// + /// Rebuilds the caches for content, media and/or members based on the content type ids specified + /// + /// + /// If not null will process content for the matching content types, if empty will process all + /// content + /// + /// + /// If not null will process content for the matching media types, if empty will process all + /// media + /// + /// + /// If not null will process content for the matching members types, if empty will process all + /// members + /// + void Rebuild( + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null); + + /// + /// Verifies the content cache by asserting that every document should have a corresponding row for edited properties and if published, + /// may have a corresponding row for published properties + /// + bool VerifyContentDbCache(); + + /// + /// Rebuilds the caches for content, media and/or members based on the content type ids specified + /// + bool VerifyMediaDbCache(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/PropertyData.cs b/src/Umbraco.PublishedCache.HybridCache/PropertyData.cs new file mode 100644 index 0000000000..80897e47ac --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/PropertyData.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +// This is for cache performance reasons, see https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0#reuse-objects +[ImmutableObject(true)] +[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys +public sealed class PropertyData +{ + private string? _culture; + private string? _segment; + + [DataMember(Order = 0)] + [JsonConverter(typeof(JsonStringInternConverter))] + [DefaultValue("")] + [JsonPropertyName("c")] + public string? Culture + { + get => _culture; + set => _culture = + value ?? throw new ArgumentNullException( + nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null + } + + [DataMember(Order = 1)] + [JsonConverter(typeof(JsonStringInternConverter))] + [DefaultValue("")] + [JsonPropertyName("s")] + public string? Segment + { + get => _segment; + set => _segment = + value ?? throw new ArgumentNullException( + nameof(value)); // TODO: or fallback to string.Empty? CANNOT be null + } + + [DataMember(Order = 2)] + [JsonPropertyName("v")] + public object? Value { get; set; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs new file mode 100644 index 0000000000..21bb651d59 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs @@ -0,0 +1,195 @@ +using Umbraco.Cms.Core.Exceptions; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +internal class PublishedContent : PublishedContentBase +{ + private IPublishedProperty[] _properties; + private readonly ContentNode _contentNode; + private IReadOnlyDictionary? _cultures; + private readonly string? _urlSegment; + private readonly IReadOnlyDictionary? _cultureInfos; + private readonly string _contentName; + private readonly bool _published; + + public PublishedContent( + ContentNode contentNode, + bool preview, + IElementsCache elementsCache, + IVariationContextAccessor variationContextAccessor) + : base(variationContextAccessor) + { + VariationContextAccessor = variationContextAccessor; + _contentNode = contentNode; + ContentData? contentData = preview ? _contentNode.DraftModel : _contentNode.PublishedModel; + if (contentData is null) + { + throw new ArgumentNullException(nameof(contentData)); + } + + _cultureInfos = contentData.CultureInfos; + _contentName = contentData.Name; + _urlSegment = contentData.UrlSegment; + _published = contentData.Published; + + var properties = new IPublishedProperty[_contentNode.ContentType.PropertyTypes.Count()]; + var i = 0; + foreach (IPublishedPropertyType propertyType in _contentNode.ContentType.PropertyTypes) + { + // add one property per property type - this is required, for the indexing to work + // if contentData supplies pdatas, use them, else use null + contentData.Properties.TryGetValue(propertyType.Alias, out PropertyData[]? propertyDatas); // else will be null + properties[i++] = new PublishedProperty(propertyType, this, propertyDatas, elementsCache, propertyType.CacheLevel); + } + + _properties = properties; + + Id = contentNode.Id; + Key = contentNode.Key; + CreatorId = contentNode.CreatorId; + CreateDate = contentNode.CreateDate; + SortOrder = contentNode.SortOrder; + WriterId = contentData.WriterId; + TemplateId = contentData.TemplateId; + UpdateDate = contentData.VersionDate; + } + + public override IPublishedContentType ContentType => _contentNode.ContentType; + + public override Guid Key { get; } + + public override IEnumerable Properties => _properties; + + public override int Id { get; } + + public override int SortOrder { get; } + + // TODO: Remove path. + public override string Path => string.Empty; + + public override int? TemplateId { get; } + + public override int CreatorId { get; } + + public override DateTime CreateDate { get; } + + public override int WriterId { get; } + + public override DateTime UpdateDate { get; } + + public bool IsPreviewing { get; } = false; + + // Needed for publishedProperty + internal IVariationContextAccessor VariationContextAccessor { get; } + + public override int Level { get; } = 0; + + public override IEnumerable ChildrenForAllCultures { get; } = Enumerable.Empty(); + + public override IPublishedContent? Parent { get; } = null!; + + + /// + public override IReadOnlyDictionary Cultures + { + get + { + if (_cultures != null) + { + return _cultures; + } + + if (!ContentType.VariesByCulture()) + { + return _cultures = new Dictionary + { + { string.Empty, new PublishedCultureInfo(string.Empty, _contentName, _urlSegment, CreateDate) }, + }; + } + + if (_cultureInfos == null) + { + throw new PanicException("_contentDate.CultureInfos is null."); + } + + + return _cultures = _cultureInfos + .ToDictionary( + x => x.Key, + x => new PublishedCultureInfo(x.Key, x.Value.Name, x.Value.UrlSegment, x.Value.Date), + StringComparer.OrdinalIgnoreCase); + } + } + + /// + public override PublishedItemType ItemType => _contentNode.ContentType.ItemType; + + public override IPublishedProperty? GetProperty(string alias) + { + var index = _contentNode.ContentType.GetPropertyIndex(alias); + if (index < 0) + { + return null; // happens when 'alias' does not match a content type property alias + } + + // should never happen - properties array must be in sync with property type + if (index >= _properties.Length) + { + throw new IndexOutOfRangeException( + "Index points outside the properties array, which means the properties array is corrupt."); + } + + IPublishedProperty property = _properties[index]; + return property; + } + + public override bool IsDraft(string? culture = null) + { + // if this is the 'published' published content, nothing can be draft + if (_published) + { + return false; + } + + // not the 'published' published content, and does not vary = must be draft + if (!ContentType.VariesByCulture()) + { + return true; + } + + // handle context culture + culture ??= VariationContextAccessor?.VariationContext?.Culture ?? string.Empty; + + // not the 'published' published content, and varies + // = depends on the culture + return _cultureInfos is not null && _cultureInfos.TryGetValue(culture, out CultureVariation? cvar) && cvar.IsDraft; + } + + public override bool IsPublished(string? culture = null) + { + // whether we are the 'draft' or 'published' content, need to determine whether + // there is a 'published' version for the specified culture (or at all, for + // invariant content items) + + // if there is no 'published' published content, no culture can be published + if (!_contentNode.HasPublished) + { + return false; + } + + // if there is a 'published' published content, and does not vary = published + if (!ContentType.VariesByCulture()) + { + return true; + } + + // handle context culture + culture ??= VariationContextAccessor.VariationContext?.Culture ?? string.Empty; + + // there is a 'published' published content, and varies + // = depends on the culture + return _contentNode.HasPublishedCulture(culture); + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs new file mode 100644 index 0000000000..4253b1a4c3 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedMember.cs @@ -0,0 +1,38 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +// note +// the whole PublishedMember thing should be refactored because as soon as a member +// is wrapped on in a model, the inner IMember and all associated properties are lost +internal class PublishedMember : PublishedContent, IPublishedMember +{ + private readonly IMember _member; + + public PublishedMember( + IMember member, + ContentNode contentNode, + IElementsCache elementsCache, + IVariationContextAccessor variationContextAccessor) + : base(contentNode, false, elementsCache, variationContextAccessor) => + _member = member; + + public string Email => _member.Email; + + public string UserName => _member.Username; + + public string? Comments => _member.Comments; + + public bool IsApproved => _member.IsApproved; + + public bool IsLockedOut => _member.IsLockedOut; + + public DateTime? LastLockoutDate => _member.LastLockoutDate; + + public DateTime CreationDate => _member.CreateDate; + + public DateTime? LastLoginDate => _member.LastLoginDate; + + public DateTime? LastPasswordChangedDate => _member.LastPasswordChangeDate; +} diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs new file mode 100644 index 0000000000..91e69d9ed7 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs @@ -0,0 +1,330 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Collections; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.HybridCache; + +internal class PublishedProperty : PublishedPropertyBase +{ + private readonly PublishedContent _content; + private readonly bool _isPreviewing; + private readonly IElementsCache _elementsCache; + private readonly bool _isMember; + private string? _valuesCacheKey; + + // the invariant-neutral source and inter values + private readonly object? _sourceValue; + private readonly ContentVariation _variations; + private readonly ContentVariation _sourceVariations; + + // the variant and non-variant object values + private bool _interInitialized; + private object? _interValue; + private CacheValues? _cacheValues; + + // the variant source and inter values + private readonly object _locko = new(); + private ConcurrentDictionary? _sourceValues; + + // initializes a published content property with a value + public PublishedProperty( + IPublishedPropertyType propertyType, + PublishedContent content, + PropertyData[]? sourceValues, + IElementsCache elementsElementsCache, + PropertyCacheLevel referenceCacheLevel = PropertyCacheLevel.Element) + : base(propertyType, referenceCacheLevel) + { + if (sourceValues != null) + { + foreach (PropertyData sourceValue in sourceValues) + { + if (sourceValue.Culture == string.Empty && sourceValue.Segment == string.Empty) + { + _sourceValue = sourceValue.Value; + } + else + { + EnsureSourceValuesInitialized(); + + _sourceValues![new CompositeStringStringKey(sourceValue.Culture, sourceValue.Segment)] + = new SourceInterValue + { + Culture = sourceValue.Culture, + Segment = sourceValue.Segment, + SourceValue = sourceValue.Value, + }; + } + } + } + + _content = content; + _isPreviewing = content.IsPreviewing; + _isMember = content.ContentType.ItemType == PublishedItemType.Member; + _elementsCache = elementsElementsCache; + + // this variable is used for contextualizing the variation level when calculating property values. + // it must be set to the union of variance (the combination of content type and property type variance). + _variations = propertyType.Variations | content.ContentType.Variations; + _sourceVariations = propertyType.Variations; + } + + // used to cache the CacheValues of this property + internal string ValuesCacheKey => _valuesCacheKey ??= PropertyCacheValues(_content.Key, Alias, _isPreviewing); + + private string PropertyCacheValues(Guid contentUid, string typeAlias, bool previewing) + { + if (previewing) + { + return "Cache.Property.CacheValues[D:" + contentUid + ":" + typeAlias + "]"; + } + + return "Cache.Property.CacheValues[P:" + contentUid + ":" + typeAlias + "]"; + } + + // determines whether a property has value + public override bool HasValue(string? culture = null, string? segment = null) + { + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); + + var value = GetSourceValue(culture, segment); + var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); + if (hasValue.HasValue) + { + return hasValue.Value; + } + + return PropertyType.IsValue(GetInterValue(culture, segment), PropertyValueLevel.Object) ?? false; + } + + public override object? GetSourceValue(string? culture = null, string? segment = null) + { + _content.VariationContextAccessor.ContextualizeVariation(_sourceVariations, _content.Id, ref culture, ref segment); + + // source values are tightly bound to the property/schema culture and segment configurations, so we need to + // sanitize the contextualized culture/segment states before using them to access the source values. + culture = _sourceVariations.VariesByCulture() ? culture : string.Empty; + segment = _sourceVariations.VariesBySegment() ? segment : string.Empty; + + if (culture == string.Empty && segment == string.Empty) + { + return _sourceValue; + } + + if (_sourceValues == null) + { + return null; + } + + return _sourceValues.TryGetValue( + new CompositeStringStringKey(culture, segment), + out SourceInterValue? sourceValue) + ? sourceValue.SourceValue + : null; + } + + private object? GetInterValue(string? culture, string? segment) + { + if (culture is "" && segment is "") + { + if (_interInitialized) + { + return _interValue; + } + + _interValue = PropertyType.ConvertSourceToInter(_content, _sourceValue, _isPreviewing); + _interInitialized = true; + return _interValue; + } + + return PropertyType.ConvertSourceToInter(_content, GetSourceValue(culture, segment), _isPreviewing); + } + + public override object? GetValue(string? culture = null, string? segment = null) + { + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); + + object? value; + CacheValue cacheValues = GetCacheValues(PropertyType.CacheLevel).For(culture, segment); + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + if (cacheValues.ObjectInitialized) + { + return cacheValues.ObjectValue; + } + + cacheValues.ObjectValue = PropertyType.ConvertInterToObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing); + cacheValues.ObjectInitialized = true; + value = cacheValues.ObjectValue; + + return value; + } + + private CacheValues GetCacheValues(PropertyCacheLevel cacheLevel) + { + CacheValues cacheValues; + IAppCache? cache; + switch (cacheLevel) + { + case PropertyCacheLevel.None: + // never cache anything + cacheValues = new CacheValues(); + break; + case PropertyCacheLevel.Snapshot: // Snapshot is obsolete, so for now treat as element + case PropertyCacheLevel.Element: + // cache within the property object itself, ie within the content object + cacheValues = _cacheValues ??= new CacheValues(); + break; + case PropertyCacheLevel.Elements: + // cache within the elements cache, unless previewing, then use the snapshot or + // elements cache (if we don't want to pollute the elements cache with short-lived + // data) depending on settings + // for members, always cache in the snapshot cache - never pollute elements cache + cache = _isMember == false ? _elementsCache : null; + cacheValues = GetCacheValues(cache); + break; + default: + throw new InvalidOperationException("Invalid cache level."); + } + + return cacheValues; + } + + private CacheValues GetCacheValues(IAppCache? cache) + { + // no cache, don't cache + if (cache == null) + { + return new CacheValues(); + } + + return (CacheValues)cache.Get(ValuesCacheKey, () => new CacheValues())!; + } + + public override object? GetDeliveryApiValue(bool expanding, string? culture = null, string? segment = null) + { + _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, ref culture, ref segment); + + object? value; + CacheValue cacheValues = GetCacheValues(expanding ? PropertyType.DeliveryApiCacheLevelForExpansion : PropertyType.DeliveryApiCacheLevel).For(culture, segment); + + + // initial reference cache level always is .Content + const PropertyCacheLevel initialCacheLevel = PropertyCacheLevel.Element; + + object? GetDeliveryApiObject() => PropertyType.ConvertInterToDeliveryApiObject(_content, initialCacheLevel, GetInterValue(culture, segment), _isPreviewing, expanding); + value = expanding + ? GetDeliveryApiExpandedObject(cacheValues, GetDeliveryApiObject) + : GetDeliveryApiDefaultObject(cacheValues, GetDeliveryApiObject); + + return value; + } + + private object? GetDeliveryApiDefaultObject(CacheValue cacheValues, Func getValue) + { + if (cacheValues.DeliveryApiDefaultObjectInitialized == false) + { + cacheValues.DeliveryApiDefaultObjectValue = getValue(); + cacheValues.DeliveryApiDefaultObjectInitialized = true; + } + + return cacheValues.DeliveryApiDefaultObjectValue; + } + + private object? GetDeliveryApiExpandedObject(CacheValue cacheValues, Func getValue) + { + if (cacheValues.DeliveryApiExpandedObjectInitialized == false) + { + cacheValues.DeliveryApiExpandedObjectValue = getValue(); + cacheValues.DeliveryApiExpandedObjectInitialized = true; + } + + return cacheValues.DeliveryApiExpandedObjectValue; + } + + private class SourceInterValue + { + private string? _culture; + private string? _segment; + + public string? Culture + { + get => _culture; + internal set => _culture = value?.ToLowerInvariant(); + } + + public string? Segment + { + get => _segment; + internal set => _segment = value?.ToLowerInvariant(); + } + + public object? SourceValue { get; set; } + } + + private class CacheValues : CacheValue + { + private readonly object _locko = new(); + private ConcurrentDictionary? _values; + + public CacheValue For(string? culture, string? segment) + { + if (culture == string.Empty && segment == string.Empty) + { + return this; + } + + if (_values == null) + { + lock (_locko) + { + _values ??= InitializeConcurrentDictionary(); + } + } + + var k = new CompositeStringStringKey(culture, segment); + + CacheValue value = _values.GetOrAdd(k, _ => new CacheValue()); + + return value; + } + } + + private class CacheValue + { + public bool ObjectInitialized { get; set; } + + public object? ObjectValue { get; set; } + + public bool DeliveryApiDefaultObjectInitialized { get; set; } + + public object? DeliveryApiDefaultObjectValue { get; set; } + + public bool DeliveryApiExpandedObjectInitialized { get; set; } + + public object? DeliveryApiExpandedObjectValue { get; set; } + } + + private static ConcurrentDictionary InitializeConcurrentDictionary() + where TKey : notnull + => new(-1, 5); + + private void EnsureSourceValuesInitialized() + { + if (_sourceValues is not null) + { + return; + } + + lock (_locko) + { + _sourceValues ??= InitializeConcurrentDictionary(); + } + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataModel.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataModel.cs new file mode 100644 index 0000000000..bb84c0998e --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataModel.cs @@ -0,0 +1,30 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; +using MessagePack; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// The content model stored in the content cache database table serialized as JSON +/// +[DataContract] // NOTE: Use DataContract annotations here to control how MessagePack serializes/deserializes the data to use INT keys +public sealed class ContentCacheDataModel +{ + [DataMember(Order = 0)] + [JsonPropertyName("pd")] + [JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter))] + [MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter))] + public Dictionary? PropertyData { get; set; } + + [DataMember(Order = 1)] + [JsonPropertyName("cd")] + [JsonConverter(typeof(JsonDictionaryStringInternIgnoreCaseConverter))] + [MessagePackFormatter(typeof(MessagePackDictionaryStringInternIgnoreCaseFormatter))] + public Dictionary? CultureData { get; set; } + + // TODO: Remove this when routing cache is in place + [DataMember(Order = 2)] + [JsonPropertyName("us")] + public string? UrlSegment { get; set; } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializationResult.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializationResult.cs new file mode 100644 index 0000000000..68b80d9847 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializationResult.cs @@ -0,0 +1,47 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// The serialization result from for which the serialized value +/// will be either a string or a byte[] +/// +public struct ContentCacheDataSerializationResult : IEquatable +{ + public ContentCacheDataSerializationResult(string? stringData, byte[]? byteData) + { + StringData = stringData; + ByteData = byteData; + } + + public string? StringData { get; } + + public byte[]? ByteData { get; } + + public static bool operator ==(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + => left.Equals(right); + + public static bool operator !=(ContentCacheDataSerializationResult left, ContentCacheDataSerializationResult right) + => !(left == right); + + public override bool Equals(object? obj) + => obj is ContentCacheDataSerializationResult result && Equals(result); + + public bool Equals(ContentCacheDataSerializationResult other) + => StringData == other.StringData && + EqualityComparer.Default.Equals(ByteData, other.ByteData); + + public override int GetHashCode() + { + var hashCode = 1910544615; + if (StringData is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(StringData); + } + + if (ByteData is not null) + { + hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(ByteData); + } + + return hashCode; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializerEntityType.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializerEntityType.cs new file mode 100644 index 0000000000..7cf61a0583 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/ContentCacheDataSerializerEntityType.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +[Flags] +public enum ContentCacheDataSerializerEntityType +{ + Document = 1, + Media = 2, + Member = 4, +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializer.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializer.cs new file mode 100644 index 0000000000..a46c667a4d --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializer.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// Serializes/Deserializes document to the SQL Database as a string +/// +/// +/// Resolved from the . This cannot be resolved from DI. +/// +internal interface IContentCacheDataSerializer +{ + /// + /// Deserialize the data into a + /// + ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published); + + /// + /// Serializes the + /// + ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializerFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializerFactory.cs new file mode 100644 index 0000000000..32d6c71795 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/IContentCacheDataSerializerFactory.cs @@ -0,0 +1,15 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +internal interface IContentCacheDataSerializerFactory +{ + /// + /// Gets or creates a new instance of + /// + /// + /// + /// This method may return the same instance, however this depends on the state of the application and if any + /// underlying data has changed. + /// This method may also be used to initialize anything before a serialization/deserialization session occurs. + /// + IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializer.cs new file mode 100644 index 0000000000..a580b07b37 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializer.cs @@ -0,0 +1,39 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +internal class JsonContentNestedDataSerializer : IContentCacheDataSerializer +{ + private static readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + + /// + public ContentCacheDataModel? Deserialize( + IReadOnlyContentBase content, + string? stringData, + byte[]? byteData, + bool published) + { + if (stringData == null && byteData != null) + { + throw new NotSupportedException( + $"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization"); + } + + return JsonSerializer.Deserialize(stringData!, _jsonSerializerOptions); + } + + /// + public ContentCacheDataSerializationResult Serialize( + IReadOnlyContentBase content, + ContentCacheDataModel model, + bool published) + { + var json = JsonSerializer.Serialize(model, _jsonSerializerOptions); + return new ContentCacheDataSerializationResult(json, null); + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..7353953f4a --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/JsonContentNestedDataSerializerFactory.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +internal class JsonContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory +{ + private readonly Lazy _serializer = new(); + + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) => _serializer.Value; +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/LazyCompressedString.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/LazyCompressedString.cs new file mode 100644 index 0000000000..84c74ab5cf --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/LazyCompressedString.cs @@ -0,0 +1,105 @@ +using System.Diagnostics; +using System.Text; +using K4os.Compression.LZ4; +using Umbraco.Cms.Core.Exceptions; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// Lazily decompresses a LZ4 Pickler compressed UTF8 string +/// +[DebuggerDisplay("{Display}")] +internal struct LazyCompressedString +{ + private readonly object _locker; + private byte[]? _bytes; + private string? _str; + + /// + /// Constructor + /// + /// LZ4 Pickle compressed UTF8 String + public LazyCompressedString(byte[] bytes) + { + _locker = new object(); + _bytes = bytes; + _str = null; + } + + /// + /// Used to display debugging output since ToString() can only be called once + /// + private string Display + { + get + { + if (_str != null) + { + return $"Decompressed: {_str}"; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return $"Decompressed: {_str}"; + } + + if (_bytes == null) + { + // This shouldn't happen + throw new PanicException("Bytes have already been cleared"); + } + + return $"Compressed Bytes: {_bytes.Length}"; + } + } + } + + public static implicit operator string(LazyCompressedString l) => l.ToString(); + + public byte[] GetBytes() + { + if (_bytes == null) + { + throw new InvalidOperationException("The bytes have already been expanded"); + } + + return _bytes; + } + + /// + /// Returns the decompressed string from the bytes. This methods can only be called once. + /// + /// + /// Throws if this is called more than once + public string DecompressString() + { + if (_str != null) + { + return _str; + } + + lock (_locker) + { + if (_str != null) + { + // double check + return _str; + } + + if (_bytes == null) + { + throw new InvalidOperationException("Bytes have already been cleared"); + } + + _str = Encoding.UTF8.GetString(LZ4Pickler.Unpickle(_bytes)); + _bytes = null; + } + + return _str; + } + + public override string ToString() => DecompressString(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs new file mode 100644 index 0000000000..7527b21e4e --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/MessagePackDictionaryStringInternIgnoreCaseFormatter.cs @@ -0,0 +1,27 @@ +using MessagePack; +using MessagePack.Formatters; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// A MessagePack formatter (deserializer) for a string key dictionary that uses for the key string comparison and interns the string. +/// +/// The type of the value. +public sealed class MessagePackDictionaryStringInternIgnoreCaseFormatter : DictionaryFormatterBase, Dictionary.Enumerator, Dictionary> +{ + /// + protected override void Add(Dictionary collection, int index, string key, TValue value, MessagePackSerializerOptions options) + => collection.Add(string.Intern(key), value); + + /// + protected override Dictionary Complete(Dictionary intermediateCollection) + => intermediateCollection; + + /// + protected override Dictionary.Enumerator GetSourceEnumerator(Dictionary source) + => source.GetEnumerator(); + + /// + protected override Dictionary Create(int count, MessagePackSerializerOptions options) + => new(count, StringComparer.OrdinalIgnoreCase); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializer.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializer.cs new file mode 100644 index 0000000000..ec4e047d29 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializer.cs @@ -0,0 +1,147 @@ +using System.Text; +using K4os.Compression.LZ4; +using MessagePack; +using MessagePack.Resolvers; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +/// +/// Serializes/Deserializes document to the SQL Database as bytes using +/// MessagePack +/// +internal sealed class MsgPackContentNestedDataSerializer : IContentCacheDataSerializer +{ + private readonly MessagePackSerializerOptions _options; + private readonly IPropertyCacheCompression _propertyOptions; + + public MsgPackContentNestedDataSerializer(IPropertyCacheCompression propertyOptions) + { + _propertyOptions = propertyOptions ?? throw new ArgumentNullException(nameof(propertyOptions)); + + MessagePackSerializerOptions? defaultOptions = ContractlessStandardResolver.Options; + IFormatterResolver? resolver = CompositeResolver.Create( + + // TODO: We want to be able to intern the strings for aliases when deserializing like we do for Newtonsoft but I'm unsure exactly how + // to do that but it would seem to be with a custom message pack resolver but I haven't quite figured out based on the docs how + // to do that since that is part of the int key -> string mapping operation, might have to see the source code to figure that one out. + // There are docs here on how to build one of these: https://github.com/neuecc/MessagePack-CSharp/blob/master/README.md#low-level-api-imessagepackformattert + // and there are a couple examples if you search on google for them but this will need to be a separate project. + // NOTE: resolver custom types first + // new ContentNestedDataResolver(), + + // finally use standard resolver + defaultOptions.Resolver); + + _options = defaultOptions + .WithResolver(resolver) + .WithCompression(MessagePackCompression.Lz4BlockArray) + .WithSecurity(MessagePackSecurity.UntrustedData); + } + + public ContentCacheDataModel? Deserialize(IReadOnlyContentBase content, string? stringData, byte[]? byteData, bool published) + { + if (byteData != null) + { + ContentCacheDataModel? cacheModel = + MessagePackSerializer.Deserialize(byteData, _options); + Expand(content, cacheModel, published); + return cacheModel; + } + + if (stringData != null) + { + // NOTE: We don't really support strings but it's possible if manually used (i.e. tests) + var bin = Convert.FromBase64String(stringData); + ContentCacheDataModel? cacheModel = MessagePackSerializer.Deserialize(bin, _options); + Expand(content, cacheModel, published); + return cacheModel; + } + + return null; + } + + public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) + { + Compress(content, model, published); + var bytes = MessagePackSerializer.Serialize(model, _options); + return new ContentCacheDataSerializationResult(null, bytes); + } + + public string ToJson(byte[] bin) + { + var json = MessagePackSerializer.ConvertToJson(bin, _options); + return json; + } + + /// + /// Used during serialization to compress properties + /// + /// + /// + /// + /// + /// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed + /// but this will go a step further and double compress property data so that it is stored in the nucache file + /// as compressed bytes and therefore will exist in memory as compressed bytes. That is, until the bytes are + /// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant + /// memory savings but could also affect performance of first rendering pages while decompression occurs. + /// + private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published) + { + if (model.PropertyData is null) + { + return; + } + + foreach (KeyValuePair propertyAliasToData in model.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) + { + foreach (PropertyData property in propertyAliasToData.Value.Where(x => + x.Value != null && x.Value is string)) + { + if (property.Value is string propertyValue) + { + property.Value = LZ4Pickler.Pickle(Encoding.UTF8.GetBytes(propertyValue)); + } + } + + foreach (PropertyData property in propertyAliasToData.Value.Where(x => + x.Value != null && x.Value is int intVal)) + { + property.Value = Convert.ToBoolean((int?)property.Value); + } + } + } + } + + /// + /// Used during deserialization to map the property data as lazy or expand the value + /// + /// + /// + /// + private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published) + { + if (nestedData.PropertyData is null) + { + return; + } + + foreach (KeyValuePair propertyAliasToData in nestedData.PropertyData) + { + if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published)) + { + foreach (PropertyData property in propertyAliasToData.Value.Where(x => x.Value != null)) + { + if (property.Value is byte[] byteArrayValue) + { + property.Value = new LazyCompressedString(byteArrayValue); + } + } + } + } + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializerFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializerFactory.cs new file mode 100644 index 0000000000..f75f83ab73 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Serialization/MsgPackContentNestedDataSerializerFactory.cs @@ -0,0 +1,69 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Serialization; + +internal class MsgPackContentNestedDataSerializerFactory : IContentCacheDataSerializerFactory +{ + private readonly IPropertyCacheCompressionOptions _compressionOptions; + private readonly IContentTypeService _contentTypeService; + private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new(); + private readonly IMediaTypeService _mediaTypeService; + private readonly IMemberTypeService _memberTypeService; + private readonly PropertyEditorCollection _propertyEditors; + + public MsgPackContentNestedDataSerializerFactory( + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + PropertyEditorCollection propertyEditors, + IPropertyCacheCompressionOptions compressionOptions) + { + _contentTypeService = contentTypeService; + _mediaTypeService = mediaTypeService; + _memberTypeService = memberTypeService; + _propertyEditors = propertyEditors; + _compressionOptions = compressionOptions; + } + + public IContentCacheDataSerializer Create(ContentCacheDataSerializerEntityType types) + { + // Depending on which entity types are being requested, we need to look up those content types + // to initialize the compression options. + // We need to initialize these options now so that any data lookups required are completed and are not done while the content cache + // is performing DB queries which will result in errors since we'll be trying to query with open readers. + // NOTE: The calls to GetAll() below should be cached if the data has not been changed. + var contentTypes = new Dictionary(); + if ((types & ContentCacheDataSerializerEntityType.Document) == ContentCacheDataSerializerEntityType.Document) + { + foreach (IContentType ct in _contentTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + if ((types & ContentCacheDataSerializerEntityType.Media) == ContentCacheDataSerializerEntityType.Media) + { + foreach (IMediaType ct in _mediaTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + if ((types & ContentCacheDataSerializerEntityType.Member) == ContentCacheDataSerializerEntityType.Member) + { + foreach (IMemberType ct in _memberTypeService.GetAll()) + { + contentTypes[ct.Id] = ct; + } + } + + var compression = + new PropertyCacheCompression(_compressionOptions, contentTypes, _propertyEditors, _isCompressedCache); + var serializer = new MsgPackContentNestedDataSerializer(compression); + + return serializer; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs new file mode 100644 index 0000000000..b0aa936793 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -0,0 +1,174 @@ +using Microsoft.Extensions.Caching.Hybrid; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +internal sealed class DocumentCacheService : IDocumentCacheService +{ + private readonly IDatabaseCacheRepository _databaseCacheRepository; + private readonly IIdKeyMap _idKeyMap; + private readonly ICoreScopeProvider _scopeProvider; + private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; + private readonly IPublishedContentFactory _publishedContentFactory; + private readonly ICacheNodeFactory _cacheNodeFactory; + + + public DocumentCacheService( + IDatabaseCacheRepository databaseCacheRepository, + IIdKeyMap idKeyMap, + ICoreScopeProvider scopeProvider, + Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, + IPublishedContentFactory publishedContentFactory, + ICacheNodeFactory cacheNodeFactory) + { + _databaseCacheRepository = databaseCacheRepository; + _idKeyMap = idKeyMap; + _scopeProvider = scopeProvider; + _hybridCache = hybridCache; + _publishedContentFactory = publishedContentFactory; + _cacheNodeFactory = cacheNodeFactory; + } + + // TODO: Stop using IdKeyMap for these, but right now we both need key and id for caching.. + public async Task GetByKeyAsync(Guid key, bool preview = false) + { + Attempt idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document); + if (idAttempt.Success is false) + { + return null; + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + GetCacheKey(key, preview), // Unique key to the cache entry + async cancel => await _databaseCacheRepository.GetContentSourceAsync(idAttempt.Result, preview)); + + scope.Complete(); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + } + + public async Task GetByIdAsync(int id, bool preview = false) + { + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); + if (keyAttempt.Success is false) + { + return null; + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry + async cancel => await _databaseCacheRepository.GetContentSourceAsync(id, preview)); + scope.Complete(); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + } + + public async Task SeedAsync(IReadOnlyCollection contentTypeKeys) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + IEnumerable contentCacheNodes = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys); + foreach (ContentCacheNode contentCacheNode in contentCacheNodes) + { + if (contentCacheNode.IsDraft) + { + continue; + } + + // TODO: Make these expiration dates configurable. + // Never expire seeded values, we cannot do TimeSpan.MaxValue sadly, so best we can do is a year. + var entryOptions = new HybridCacheEntryOptions + { + Expiration = TimeSpan.FromDays(365), + LocalCacheExpiration = TimeSpan.FromDays(365), + }; + + await _hybridCache.SetAsync( + GetCacheKey(contentCacheNode.Key, false), + contentCacheNode, + entryOptions); + } + + scope.Complete(); + } + + public async Task HasContentByIdAsync(int id, bool preview = false) + { + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); + if (keyAttempt.Success is false) + { + return false; + } + + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry + cancel => ValueTask.FromResult(null)); + + if (contentCacheNode is null) + { + await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, preview)); + } + + return contentCacheNode is not null; + } + + public async Task RefreshContentAsync(IContent content) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + // Always set draft node + // We have nodes seperate in the cache, cause 99% of the time, you are only using one + // and thus we won't get too much data when retrieving from the cache. + var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)); + await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState); + + if (content.PublishedState == PublishedState.Publishing) + { + var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)); + await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState); + } + + scope.Complete(); + } + + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; + + public async Task DeleteItemAsync(int id) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + await _databaseCacheRepository.DeleteContentItemAsync(id); + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); + await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, true)); + await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, false)); + _idKeyMap.ClearCache(keyAttempt.Result); + _idKeyMap.ClearCache(id); + scope.Complete(); + } + + public void Rebuild(IReadOnlyCollection contentTypeKeys) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + _databaseCacheRepository.Rebuild(contentTypeKeys.ToList()); + IEnumerable contentByContentTypeKey = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys.Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.DocumentType).Result)); + + foreach (ContentCacheNode content in contentByContentTypeKey) + { + _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)).GetAwaiter().GetResult(); + + if (content.IsDraft is false) + { + _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)).GetAwaiter().GetResult(); + } + } + + scope.Complete(); + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs new file mode 100644 index 0000000000..b4cb3019af --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DomainCacheService.cs @@ -0,0 +1,111 @@ +using System.Collections.Concurrent; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Extensions; + + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +public class DomainCacheService : IDomainCacheService +{ + private readonly IDomainService _domainService; + private readonly ICoreScopeProvider _coreScopeProvider; + private readonly ConcurrentDictionary _domains; + + public DomainCacheService(IDomainService domainService, ICoreScopeProvider coreScopeProvider) + { + _domainService = domainService; + _coreScopeProvider = coreScopeProvider; + _domains = new ConcurrentDictionary(); + } + + public IEnumerable GetAll(bool includeWildcards) + { + return includeWildcards == false + ? _domains.Select(x => x.Value).Where(x => x.IsWildcard == false).OrderBy(x => x.SortOrder) + : _domains.Select(x => x.Value).OrderBy(x => x.SortOrder); + } + + /// + public IEnumerable GetAssigned(int documentId, bool includeWildcards = false) + { + // probably this could be optimized with an index + // but then we'd need a custom DomainStore of some sort + IEnumerable list = _domains.Select(x => x.Value).Where(x => x.ContentId == documentId); + if (includeWildcards == false) + { + list = list.Where(x => x.IsWildcard == false); + } + + return list.OrderBy(x => x.SortOrder); + } + + /// + public bool HasAssigned(int documentId, bool includeWildcards = false) + => documentId > 0 && GetAssigned(documentId, includeWildcards).Any(); + + public void Refresh(DomainCacheRefresher.JsonPayload[] payloads) + { + foreach (DomainCacheRefresher.JsonPayload payload in payloads) + { + switch (payload.ChangeType) + { + case DomainChangeTypes.RefreshAll: + using (ICoreScope scope = _coreScopeProvider.CreateCoreScope()) + { + scope.ReadLock(Constants.Locks.Domains); + LoadDomains(); + scope.Complete(); + } + + break; + case DomainChangeTypes.Remove: + _domains.Remove(payload.Id, out _); + break; + case DomainChangeTypes.Refresh: + IDomain? domain = _domainService.GetById(payload.Id); + if (domain == null) + { + continue; + } + + if (domain.RootContentId.HasValue == false) + { + continue; // anomaly + } + + var culture = domain.LanguageIsoCode; + if (string.IsNullOrWhiteSpace(culture)) + { + continue; // anomaly + } + + var newDomain = new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder); + + // Feels wierd to use key and oldvalue, but we're using neither when updating. + _domains.AddOrUpdate( + domain.Id, + new Domain(domain.Id, domain.DomainName, domain.RootContentId.Value, culture, domain.IsWildcard, domain.SortOrder), + (key, oldValue) => newDomain); + break; + } + } + } + + private void LoadDomains() + { + IEnumerable domains = _domainService.GetAll(true); + foreach (Domain domain in domains + .Where(x => x.RootContentId.HasValue && x.LanguageIsoCode.IsNullOrWhiteSpace() == false) + .Select(x => new Domain(x.Id, x.DomainName, x.RootContentId!.Value, x.LanguageIsoCode!, x.IsWildcard, x.SortOrder))) + { + _domains.AddOrUpdate(domain.Id, domain, (key, oldValue) => domain); + } + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs new file mode 100644 index 0000000000..794c22b261 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs @@ -0,0 +1,21 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +public interface IDocumentCacheService +{ + Task GetByKeyAsync(Guid key, bool preview = false); + + Task GetByIdAsync(int id, bool preview = false); + + Task SeedAsync(IReadOnlyCollection contentTypeKeys); + + Task HasContentByIdAsync(int id, bool preview = false); + + Task RefreshContentAsync(IContent content); + + Task DeleteItemAsync(int id); + + void Rebuild(IReadOnlyCollection contentTypeKeys); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs new file mode 100644 index 0000000000..ad5ed2d769 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs @@ -0,0 +1,17 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +public interface IMediaCacheService +{ + Task GetByKeyAsync(Guid key); + + Task GetByIdAsync(int id); + + Task HasContentByIdAsync(int id); + + Task RefreshMediaAsync(IMedia media); + + Task DeleteItemAsync(int id); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMemberCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IMemberCacheService.cs new file mode 100644 index 0000000000..9f8dd94942 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IMemberCacheService.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +public interface IMemberCacheService +{ + Task Get(IMember member); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs new file mode 100644 index 0000000000..9f62072c0d --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -0,0 +1,120 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +internal class MediaCacheService : IMediaCacheService +{ + private readonly IDatabaseCacheRepository _databaseCacheRepository; + private readonly IIdKeyMap _idKeyMap; + private readonly ICoreScopeProvider _scopeProvider; + private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; + private readonly IPublishedContentFactory _publishedContentFactory; + private readonly ICacheNodeFactory _cacheNodeFactory; + + public MediaCacheService( + IDatabaseCacheRepository databaseCacheRepository, + IIdKeyMap idKeyMap, + ICoreScopeProvider scopeProvider, + Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, + IPublishedContentFactory publishedContentFactory, + ICacheNodeFactory cacheNodeFactory) + { + _databaseCacheRepository = databaseCacheRepository; + _idKeyMap = idKeyMap; + _scopeProvider = scopeProvider; + _hybridCache = hybridCache; + _publishedContentFactory = publishedContentFactory; + _cacheNodeFactory = cacheNodeFactory; + } + + public async Task GetByKeyAsync(Guid key) + { + Attempt idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Media); + if (idAttempt.Success is false) + { + return null; + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + $"{key}", // Unique key to the cache entry + async cancel => await _databaseCacheRepository.GetMediaSourceAsync(idAttempt.Result)); + + scope.Complete(); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode); + } + + public async Task GetByIdAsync(int id) + { + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media); + if (keyAttempt.Success is false) + { + return null; + } + + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + $"{keyAttempt.Result}", // Unique key to the cache entry + async cancel => await _databaseCacheRepository.GetMediaSourceAsync(id)); + scope.Complete(); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedMedia(contentCacheNode); + } + + public async Task HasContentByIdAsync(int id) + { + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media); + if (keyAttempt.Success is false) + { + return false; + } + + ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( + $"{keyAttempt.Result}", // Unique key to the cache entry + cancel => ValueTask.FromResult(null)); + + if (contentCacheNode is null) + { + await _hybridCache.RemoveAsync($"{keyAttempt.Result}"); + } + + return contentCacheNode is not null; + } + + + public async Task RefreshMediaAsync(IMedia media) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + // Always set draft node + // We have nodes seperate in the cache, cause 99% of the time, you are only using one + // and thus we won't get too much data when retrieving from the cache. + var cacheNode = _cacheNodeFactory.ToContentCacheNode(media); + await _hybridCache.SetAsync(GetCacheKey(media.Key, false), cacheNode); + await _databaseCacheRepository.RefreshMediaAsync(cacheNode); + scope.Complete(); + } + + public async Task DeleteItemAsync(int id) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + await _databaseCacheRepository.DeleteContentItemAsync(id); + Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media); + if (keyAttempt.Success) + { + await _hybridCache.RemoveAsync(keyAttempt.Result.ToString()); + } + + _idKeyMap.ClearCache(keyAttempt.Result); + _idKeyMap.ClearCache(id); + + scope.Complete(); + } + + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MemberCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MemberCacheService.cs new file mode 100644 index 0000000000..f7bc3896fb --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MemberCacheService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; + +namespace Umbraco.Cms.Infrastructure.HybridCache.Services; + +internal class MemberCacheService : IMemberCacheService +{ + private readonly IPublishedContentFactory _publishedContentFactory; + + public MemberCacheService(IPublishedContentFactory publishedContentFactory) => _publishedContentFactory = publishedContentFactory; + + public async Task Get(IMember member) => member is null ? null : _publishedContentFactory.ToPublishedMember(member); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj new file mode 100644 index 0000000000..41fb4becbc --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj @@ -0,0 +1,35 @@ + + + + Umbraco.Cms.PublishedCache.HybridCache + Umbraco CMS - Published cache - HybridCache + Contains the published cache assembly needed to run Umbraco CMS. + Umbraco.Cms.Infrastructure.HybridCache + + false + + + + + + + + + + + <_Parameter1>Umbraco.Tests + + + <_Parameter1>Umbraco.Tests.Integration + + + <_Parameter1>DynamicProxyGenAssembly2 + + + + + + + + + diff --git a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs index b50a0a71ce..8c4b369234 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentCache.cs @@ -56,6 +56,12 @@ public class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigab public IPublishedContent? GetByRoute(string route, bool? hideTopLevelNode = null, string? culture = null) => GetByRoute(PreviewDefault, route, hideTopLevelNode, culture); + public Task GetByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); + + public Task GetByIdAsync(Guid key, bool preview = false) => throw new NotImplementedException(); + + public Task HasByIdAsync(int id, bool preview = false) => throw new NotImplementedException(); + public IPublishedContent? GetByRoute(bool preview, string route, bool? hideTopLevelNode = null, string? culture = null) { if (route == null) diff --git a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs index 9b3b9704cf..4c65255aa7 100644 --- a/src/Umbraco.PublishedCache.NuCache/MediaCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/MediaCache.cs @@ -99,4 +99,10 @@ public class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableDa public override IPublishedContentType? GetContentType(Guid key) => _snapshot.GetContentType(key); #endregion + + public Task GetByIdAsync(int id) => throw new NotImplementedException(); + + public Task GetByKeyAsync(Guid key) => throw new NotImplementedException(); + + public Task HasByIdAsync(int id) => throw new NotImplementedException(); } diff --git a/src/Umbraco.PublishedCache.NuCache/MemberCache.cs b/src/Umbraco.PublishedCache.NuCache/MemberCache.cs index 8a23f7c1c0..0aa89b2c42 100644 --- a/src/Umbraco.PublishedCache.NuCache/MemberCache.cs +++ b/src/Umbraco.PublishedCache.NuCache/MemberCache.cs @@ -32,10 +32,11 @@ public class MemberCache : IPublishedMemberCache, IDisposable public IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Member, id); public IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Member, alias); + public Task GetAsync(IMember member) => throw new NotImplementedException(); - public IPublishedContent? Get(IMember member) + public IPublishedMember? Get(IMember member) => - PublishedMember.Create( + (IPublishedMember?)PublishedMember.Create( member, GetContentType(member.ContentTypeId), _previewDefault, @@ -58,7 +59,7 @@ public class MemberCache : IPublishedMemberCache, IDisposable { if (disposing) { - _contentTypeCache.Dispose(); + // _contentTypeCache.Dispose(); } _disposedValue = true; diff --git a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs index 0d67d2a8e3..a65b66cd30 100644 --- a/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs +++ b/src/Umbraco.PublishedCache.NuCache/Persistence/NuCacheContentRepository.cs @@ -337,6 +337,7 @@ AND cmsContentNu.nodeId IS NULL Sql? sql = SqlMediaSourcesSelect(SqlContentSourcesSelectUmbracoNodeJoin) .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) .Append(SqlWhereNodeIdX(SqlContext, id)) + .Append(SqlWhereNodeIdX(SqlContext, id)) .Append(SqlOrderByLevelIdSortOrder(SqlContext)); IContentCacheDataSerializer serializer = diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs b/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs index 31f312f7b8..32d0ff7039 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedMember.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache; // note // the whole PublishedMember thing should be refactored because as soon as a member // is wrapped on in a model, the inner IMember and all associated properties are lost -internal class PublishedMember : PublishedContent +internal class PublishedMember : PublishedContent, IPublishedMember { private PublishedMember( IMember member, diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 6754577968..0816e655e0 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -100,6 +100,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer } // Check if there is no existing content and return the no content controller + // FIXME: This should be changed to route cache, so instead, if there are any routes, we know there is content. if (!umbracoContext.Content?.HasContent() ?? false) { return new RouteValueDictionary diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 0ddfbbce20..c8e77d9603 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -5,16 +5,16 @@ - - + + - - - - - - + + + + + + diff --git a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs new file mode 100644 index 0000000000..92f65bbc39 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs @@ -0,0 +1,172 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; +using Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class ContentEditingBuilder + : BuilderBase, + IWithInvariantNameBuilder, + IWithInvariantPropertiesBuilder, + IWithVariantsBuilder, + IWithKeyBuilder, + IWithContentTypeKeyBuilder, + IWithParentKeyBuilder, + IWithTemplateKeyBuilder +{ + private IContentType _contentType; + private ContentTypeBuilder _contentTypeBuilder; + private IEnumerable _invariantProperties = []; + private IEnumerable _variants = []; + private Guid _contentTypeKey; + private Guid? _parentKey; + private Guid? _templateKey; + private Guid? _key; + private string _invariantName; + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + string IWithInvariantNameBuilder.InvariantName + { + get => _invariantName; + set => _invariantName = value; + } + + IEnumerable IWithInvariantPropertiesBuilder.InvariantProperties + { + get => _invariantProperties; + set => _invariantProperties = value; + } + + IEnumerable IWithVariantsBuilder.Variants + { + get => _variants; + set => _variants = value; + } + + Guid? IWithParentKeyBuilder.ParentKey + { + get => _parentKey; + set => _parentKey = value; + } + + Guid IWithContentTypeKeyBuilder.ContentTypeKey + { + get => _contentTypeKey; + set => _contentTypeKey = value; + } + + Guid? IWithTemplateKeyBuilder.TemplateKey + { + get => _templateKey; + set => _templateKey = value; + } + + public ContentEditingBuilder WithInvariantName(string invariantName) + { + _invariantName = invariantName; + return this; + } + + public ContentEditingBuilder WithInvariantProperty(string alias, object value) + { + var property = new PropertyValueModel { Alias = alias, Value = value }; + _invariantProperties = _invariantProperties.Concat(new[] { property }); + return this; + } + + public ContentEditingBuilder AddVariant(string culture, string segment, string name, + IEnumerable properties) + { + var variant = new VariantModel { Culture = culture, Segment = segment, Name = name, Properties = properties }; + _variants = _variants.Concat(new[] { variant }); + return this; + } + + public ContentEditingBuilder WithParentKey(Guid parentKey) + { + _parentKey = parentKey; + return this; + } + + public ContentEditingBuilder WithTemplateKey(Guid templateKey) + { + _templateKey = templateKey; + return this; + } + + public ContentEditingBuilder WithContentType(IContentType contentType) + { + _contentTypeBuilder = null; + _contentType = contentType; + return this; + } + + public override ContentCreateModel Build() + { + var key = _key ?? Guid.NewGuid(); + var parentKey = _parentKey; + var templateKey = _templateKey; + var invariantName = _invariantName ?? Guid.NewGuid().ToString(); + var invariantProperties = _invariantProperties; + var variants = _variants; + + if (_contentTypeBuilder is null && _contentType is null) + { + throw new InvalidOperationException( + "A content item cannot be constructed without providing a content type. Use AddContentType() or WithContentType()."); + } + + var contentType = _contentType ?? _contentTypeBuilder.Build(); + var content = new ContentCreateModel(); + + content.InvariantName = invariantName; + if (parentKey is not null) + { + content.ParentKey = parentKey; + } + + if (templateKey is not null) + { + content.TemplateKey = templateKey; + } + + content.ContentTypeKey = contentType.Key; + content.Key = key; + content.InvariantProperties = invariantProperties; + content.Variants = variants; + + return content; + } + + public static ContentCreateModel CreateBasicContent(IContentType contentType, Guid? key) => + new ContentEditingBuilder() + .WithKey(key) + .WithContentType(contentType) + .WithInvariantName("Home") + .Build(); + + public static ContentCreateModel CreateSimpleContent(IContentType contentType) => + new ContentEditingBuilder() + .WithContentType(contentType) + .WithInvariantName("Home") + .WithInvariantProperty("title", "Welcome to our Home page") + .Build(); + + public static ContentCreateModel CreateSimpleContent(IContentType contentType, string name, Guid? parentKey) => + new ContentEditingBuilder() + .WithContentType(contentType) + .WithInvariantName(name) + .WithParentKey(parentKey) + .WithInvariantProperty("title", "Welcome to our Home page") + .Build(); +} diff --git a/tests/Umbraco.Tests.Common/Builders/Extensions/ContentEditingBuilderExtensions.cs b/tests/Umbraco.Tests.Common/Builders/Extensions/ContentEditingBuilderExtensions.cs new file mode 100644 index 0000000000..a02c4e5b12 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Extensions/ContentEditingBuilderExtensions.cs @@ -0,0 +1,58 @@ +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; +using Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +namespace Umbraco.Cms.Tests.Common.Builders.Extensions; + +public static class ContentEditingBuilderExtensions +{ + public static T WithInvariantName(this T Builder, string invariantName) + where T : IWithInvariantNameBuilder + { + Builder.InvariantName = invariantName; + return Builder; + } + + public static T WithInvariantProperties(this T Builder, IEnumerable invariantProperties) + where T : IWithInvariantPropertiesBuilder + { + Builder.InvariantProperties = invariantProperties; + return Builder; + } + + public static T WithVariants(this T Builder, IEnumerable variants) + where T : IWithVariantsBuilder + { + Builder.Variants = variants; + return Builder; + } + + public static T WithKey(this T Builder, Guid? key) + where T : IWithKeyBuilder + { + Builder.Key = key; + return Builder; + } + + public static T WithContentTypeKey(this T Builder, Guid contentTypeKey) + where T : IWithContentTypeKeyBuilder + { + Builder.ContentTypeKey = contentTypeKey; + return Builder; + } + + public static T WithParentKey(this T Builder, Guid? parentKey) + where T : IWithParentKeyBuilder + { + Builder.ParentKey = parentKey; + return Builder; + } + + + public static T WithTemplateKey(this T Builder, Guid? templateKey) + where T : IWithTemplateKeyBuilder + { + Builder.TemplateKey = templateKey; + return Builder; + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithContentTypeKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithContentTypeKeyBuilder.cs new file mode 100644 index 0000000000..cdb743ca67 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithContentTypeKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +public interface IWithContentTypeKeyBuilder +{ + public Guid ContentTypeKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantNameBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantNameBuilder.cs new file mode 100644 index 0000000000..27698b1395 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantNameBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +public interface IWithInvariantNameBuilder +{ + public string? InvariantName { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantPropertiesBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantPropertiesBuilder.cs new file mode 100644 index 0000000000..7320ac6523 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithInvariantPropertiesBuilder.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +public interface IWithInvariantPropertiesBuilder +{ + public IEnumerable InvariantProperties { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithParentKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithParentKeyBuilder.cs new file mode 100644 index 0000000000..e4fa282a7c --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithParentKeyBuilder.cs @@ -0,0 +1,9 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithParentKeyBuilder +{ + Guid? ParentKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithTemplateKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithTemplateKeyBuilder.cs new file mode 100644 index 0000000000..0b05a70548 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithTemplateKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +public interface IWithTemplateKeyBuilder +{ + public Guid? TemplateKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithVariantsBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithVariantsBuilder.cs new file mode 100644 index 0000000000..ca4e54f1c4 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/ContentCreateModel/IWithVariantsBuilder.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces.ContentCreateModel; + +public interface IWithVariantsBuilder +{ + public IEnumerable Variants { get; set; } +} diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index cf321d9498..b4437397d3 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -257,6 +257,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest .AddUmbracoCore() .AddWebComponents() .AddNuCache() + .AddUmbracoHybridCache() .AddBackOfficeCore() .AddBackOfficeAuthentication() .AddBackOfficeIdentity() @@ -298,7 +299,7 @@ namespace Umbraco.Cms.Tests.Integration.TestServerTest protected virtual void CustomMvcSetup(IMvcBuilder mvcBuilder) { - + } /// diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs new file mode 100644 index 0000000000..10dd0cb467 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs @@ -0,0 +1,133 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Testing; + +public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrationTest +{ + protected IContentTypeService ContentTypeService => GetRequiredService(); + + protected ITemplateService TemplateService => GetRequiredService(); + + private ContentEditingService ContentEditingService => + (ContentEditingService)GetRequiredService(); + + private ContentPublishingService ContentPublishingService => + (ContentPublishingService)GetRequiredService(); + + + protected ContentCreateModel Subpage2 { get; private set; } + protected ContentCreateModel Subpage3 { get; private set; } + + protected ContentCreateModel Subpage { get; private set; } + + protected ContentCreateModel Textpage { get; private set; } + + protected ContentScheduleCollection ContentSchedule { get; private set; } + + protected CultureAndScheduleModel CultureAndSchedule { get; private set; } + + protected int TextpageId { get; private set; } + + protected int SubpageId { get; private set; } + + protected int Subpage2Id { get; private set; } + + protected int Subpage3Id { get; private set; } + + protected ContentType ContentType { get; private set; } + + [SetUp] + public new void Setup() => CreateTestData(); + + protected async void CreateTestData() + { + // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. + var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + // Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type) + ContentType = + ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); + ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); + ContentType.AllowedAsRoot = true; + ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; + var contentTypeResult = await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); + Assert.IsTrue(contentTypeResult.Success); + + // Create and Save Content "Homepage" based on "umbTextpage" -> 1053 + Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType); + Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + var createContentResultTextPage = await ContentEditingService.CreateAsync(Textpage, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultTextPage.Success); + + if (!Textpage.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultTextPage.Result.Content != null) + { + TextpageId = createContentResultTextPage.Result.Content.Id; + } + + // Sets the culture and schedule for the content, in this case, we are publishing immediately for all cultures + ContentSchedule = new ContentScheduleCollection(); + CultureAndSchedule = new CultureAndScheduleModel + { + CulturesToPublishImmediately = new HashSet { "*" }, Schedules = ContentSchedule, + }; + + // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 + Subpage = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Key); + var createContentResultSubPage = await ContentEditingService.CreateAsync(Subpage, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultSubPage.Success); + + if (!Subpage.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultSubPage.Result.Content != null) + { + SubpageId = createContentResultSubPage.Result.Content.Id; + } + + await ContentPublishingService.PublishAsync(Subpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 + Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Key); + var createContentResultSubPage2 = await ContentEditingService.CreateAsync(Subpage2, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultSubPage2.Success); + if (!Subpage2.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultSubPage2.Result.Content != null) + { + Subpage2Id = createContentResultSubPage2.Result.Content.Id; + } + + Subpage3 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Key); + var createContentResultSubPage3 = await ContentEditingService.CreateAsync(Subpage3, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultSubPage3.Success); + if (!Subpage3.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultSubPage3.Result.Content != null) + { + Subpage3Id = createContentResultSubPage3.Result.Content.Id; + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs new file mode 100644 index 0000000000..f0c70c5911 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService(); + + [Test] + public async Task Can_Get_Draft_Content_By_Id() + { + //Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + ContentType.RemovePropertyType("title"); + ContentTypeService.Save(ContentType); + + // Assert + var newTextPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + Assert.IsNull(newTextPage.Value("title")); + } + + [Test] + public async Task Can_Get_Draft_Content_By_Key() + { + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + ContentType.RemovePropertyType("title"); + ContentTypeService.Save(ContentType); + //Assert + var newTextPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + Assert.IsNull(newTextPage.Value("title")); + } + + [Test] + public async Task Content_Gets_Removed_When_DocumentType_Is_Deleted() + { + // Load into cache + var textpage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true); + Assert.IsNotNull(textpage); + + await ContentTypeService.DeleteAsync(textpage.ContentType.Key, Constants.Security.SuperUserKey); + + var textpageAgain = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true); + Assert.IsNull(textpageAgain); + } + + + // TODO: Copy this into PublishedContentTypeCache + [Test] + public async Task Can_Get_Published_DocumentType_By_Key() + { + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); + Assert.IsNotNull(contentType); + var contentTypeAgain = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); + Assert.IsNotNull(contentType); + } + + [Test] + public async Task Published_DocumentType_Gets_Deleted() + { + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); + Assert.IsNotNull(contentType); + + await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey); + // PublishedContentTypeCache just explodes if it doesn't exist + Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs new file mode 100644 index 0000000000..ac7c55604f --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -0,0 +1,205 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.HybridCache; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent +{ + private IPublishedContentCache _mockedCache; + private Mock _mockedNucacheRepository; + private IDocumentCacheService _mockDocumentCacheService; + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + [SetUp] + public void SetUp() + { + _mockedNucacheRepository = new Mock(); + + var contentData = new ContentData( + Textpage.Name, + null, + 1, + Textpage.UpdateDate, + Textpage.CreatorId, + -1, + false, + new Dictionary(), + null); + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync( + new ContentCacheNode() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = true, + }); + + _mockedNucacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny>())).Returns( + new List() + { + new() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = false, + }, + }); + + _mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny())); + + _mockDocumentCacheService = new DocumentCacheService( + _mockedNucacheRepository.Object, + GetRequiredService(), + GetRequiredService(), + GetRequiredService(), + GetRequiredService(), + GetRequiredService()); + + _mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService()); + } + + [Test] + public async Task Content_Is_Cached_By_Key() + { + var hybridCache = GetRequiredService(); + await hybridCache.RemoveAsync($"{Textpage.Key}+draft"); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true); + var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Key, true); + AssertTextPage(textPage); + AssertTextPage(textPage2); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Test] + public async Task Content_Is_Cached_By_Id() + { + var hybridCache = GetRequiredService(); + await hybridCache.RemoveAsync($"{Textpage.Key}+draft"); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true); + var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Id, true); + AssertTextPage(textPage); + AssertTextPage(textPage2); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Test] + public async Task Content_Is_Seeded_By_Id() + { + var schedule = new CultureAndScheduleModel + { + CulturesToPublishImmediately = new HashSet { "*" }, Schedules = new ContentScheduleCollection(), + }; + + var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + Textpage.Published = true; + await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + + await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Id); + AssertTextPage(textPage); + + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(0)); + } + + [Test] + public async Task Content_Is_Seeded_By_Key() + { + var schedule = new CultureAndScheduleModel + { + CulturesToPublishImmediately = new HashSet { "*" }, Schedules = new ContentScheduleCollection(), + }; + + var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); + Textpage.Published = true; + await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + + await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Key); + AssertTextPage(textPage); + + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(0)); + } + + [Test] + public async Task Content_Is_Not_Seeded_If_Unpublished_By_Id() + { + + await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + + await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true); + AssertTextPage(textPage); + + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + [Test] + public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key() + { + await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + + await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true); + AssertTextPage(textPage); + + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + } + + private void AssertTextPage(IPublishedContent textPage) + { + Assert.Multiple(() => + { + Assert.IsNotNull(textPage); + Assert.AreEqual(Textpage.Name, textPage.Name); + Assert.AreEqual(Textpage.Published, textPage.IsPublished()); + }); + AssertProperties(Textpage.Properties, textPage.Properties); + } + + private void AssertProperties(IPropertyCollection propertyCollection, IEnumerable publishedProperties) + { + foreach (var prop in propertyCollection) + { + AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias)); + } + } + + private void AssertProperty(IProperty property, IPublishedProperty publishedProperty) + { + Assert.Multiple(() => + { + Assert.AreEqual(property.Alias, publishedProperty.Alias); + Assert.AreEqual(property.PropertyType.Alias, publishedProperty.PropertyType.Alias); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs new file mode 100644 index 0000000000..0cfc342917 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs @@ -0,0 +1,185 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private ICacheManager CacheManager => GetRequiredService(); + + private ITemplateService TemplateService => GetRequiredService(); + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + + [Test] + public async Task Can_Get_Value_From_ContentPicker() + { + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + var textPage = await CreateTextPageDocument(template.Id); + var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + + var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + + IPublishedContent contentPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); + Assert.AreEqual(textPage.Key, contentPickerValue.Key); + Assert.AreEqual(textPage.Id, contentPickerValue.Id); + Assert.AreEqual(textPage.Name, contentPickerValue.Name); + Assert.AreEqual("The title value", contentPickerValue.Properties.First(x => x.Alias == "title").GetValue()); + } + + [Test] + public async Task Can_Get_Value_From_Updated_ContentPicker() + { + var template = TemplateBuilder.CreateTextPageTemplate(); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + var textPage = await CreateTextPageDocument(template.Id); + var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + + // Get for caching + var notUpdatedContent = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + + IPublishedContent contentPickerValue = (IPublishedContent)notUpdatedContent.Value("contentPicker"); + Assert.AreEqual("The title value", contentPickerValue.Properties.First(x => x.Alias == "title").GetValue()); + + // Update content + var updateModel = new ContentUpdateModel + { + InvariantName = "Root Create", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "Updated title" }, + new PropertyValueModel { Alias = "bodyText", Value = "The body text" } + }, + }; + + var updateResult = await ContentEditingService.UpdateAsync(textPage.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updateResult.Success); + + var publishResult = await ContentPublishingService.PublishAsync( + updateResult.Result.Content!.Key, + new CultureAndScheduleModel() + { + CulturesToPublishImmediately = new HashSet {"*"}, + Schedules = new ContentScheduleCollection(), + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishResult); + + var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + IPublishedContent updatedPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); + + + Assert.AreEqual(textPage.Key, updatedPickerValue.Key); + Assert.AreEqual(textPage.Id, updatedPickerValue.Id); + Assert.AreEqual(textPage.Name, updatedPickerValue.Name); + Assert.AreEqual("Updated title", updatedPickerValue.Properties.First(x => x.Alias == "title").GetValue()); + } + + private async Task CreateContentPickerDocument(int templateId, Guid textPageKey) + { + var builder = new ContentTypeBuilder(); + var pickerContentType = (ContentType)builder + .WithAlias("test") + .WithName("TestName") + .AddAllowedTemplate() + .WithId(templateId) + .Done() + .AddPropertyGroup() + .WithName("Content") + .WithSupportsPublishing(true) + .AddPropertyType() + .WithAlias("contentPicker") + .WithName("Content Picker") + .WithDataTypeId(1046) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.ContentPicker) + .WithValueStorageType(ValueStorageType.Integer) + .WithSortOrder(16) + .Done() + .Done() + .Build(); + + pickerContentType.AllowedAsRoot = true; + ContentTypeService.Save(pickerContentType); + + + var createOtherModel = new ContentCreateModel + { + ContentTypeKey = pickerContentType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Test Create", + InvariantProperties = new[] { new PropertyValueModel { Alias = "contentPicker", Value = textPageKey }, }, + }; + + var result = await ContentEditingService.CreateAsync(createOtherModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + Assert.AreEqual(ContentEditingOperationStatus.Success, result.Status); + + var publishResult = await ContentPublishingService.PublishAsync( + result.Result.Content!.Key, + new CultureAndScheduleModel() + { + CulturesToPublishImmediately = new HashSet {"*"}, + Schedules = new ContentScheduleCollection(), + }, + Constants.Security.SuperUserKey); + + return result.Result.Content; + } + + private async Task CreateTextPageDocument(int templateId) + { + var textContentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: templateId); + textContentType.AllowedAsRoot = true; + ContentTypeService.Save(textContentType); + + var createModel = new ContentCreateModel + { + ContentTypeKey = textContentType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Root Create", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The title value" }, + new PropertyValueModel { Alias = "bodyText", Value = "The body text" } + }, + }; + + var createResult = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + + var publishResult = await ContentPublishingService.PublishAsync( + createResult.Result.Content!.Key, + new CultureAndScheduleModel() + { + CulturesToPublishImmediately = new HashSet {"*"}, + Schedules = new ContentScheduleCollection(), + }, + Constants.Security.SuperUserKey); + + Assert.IsTrue(publishResult.Success); + return createResult.Result.Content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs new file mode 100644 index 0000000000..8f2ad58ad6 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheScopeTests.cs @@ -0,0 +1,85 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheScopeTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private ICoreScopeProvider ICoreScopeProvider => GetRequiredService(); + + [Test] + public async Task Can_Get_Correct_Content_After_Rollback_With_Id() + { + using (ICoreScopeProvider.CreateCoreScope()) + { + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + } + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId); + + // Published page should not be in cache, as we rolled scope back. + Assert.IsNull(textPage); + } + + [Test] + public async Task Can_Get_Correct_Content_After_Rollback_With_Key() + { + using (ICoreScopeProvider.CreateCoreScope()) + { + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + } + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + + // Published page should not be in cache, as we rolled scope back. + Assert.IsNull(textPage); + } + + [Test] + public async Task Can_Get_Document_After_Scope_Complete_With_Id() + { + using (var scope = ICoreScopeProvider.CreateCoreScope()) + { + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + scope.Complete(); + } + + // Act + var publishedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId); + + // Published page should not be in cache, as we rolled scope back. + Assert.IsNotNull(publishedPage); + } + + [Test] + public async Task Can_Get_Document_After_Scope_Completes_With_Key() + { + using (var scope = ICoreScopeProvider.CreateCoreScope()) + { + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + scope.Complete(); + } + + // Act + var publishedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + + // Published page should not be in cache, as we rolled scope back. + Assert.IsNotNull(publishedPage); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs new file mode 100644 index 0000000000..7d8d4123e1 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -0,0 +1,518 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IContentPublishingService ContentPublishingService => GetRequiredService(); + + private const string NewName = "New Name"; + private const string NewTitle = "New Title"; + + + // Create CRUD Tests for Content, Also cultures. + + [Test] + public async Task Can_Get_Draft_Content_By_Id() + { + //Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + //Assert + AssertTextPage(textPage); + } + + [Test] + public async Task Can_Get_Draft_Content_By_Key() + { + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + AssertTextPage(textPage); + } + + [Test] + public async Task Can_Get_Published_Content_By_Id() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId); + + // Assert + AssertTextPage(textPage); + } + + [Test] + public async Task Can_Get_Published_Content_By_Key() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + + // Assert + AssertTextPage(textPage); + } + + [Test] + public async Task Can_Get_Draft_Of_Published_Content_By_Id() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + AssertTextPage(textPage); + Assert.IsFalse(textPage.IsPublished()); + } + + [Test] + public async Task Can_Get_Draft_Of_Published_Content_By_Key() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + AssertTextPage(textPage); + Assert.IsFalse(textPage.IsPublished()); + } + + [Test] + public async Task Can_Get_Updated_Draft_Content_By_Id() + { + // Arrange + Textpage.InvariantName = NewName; + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = NewName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var updatedPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + Assert.AreEqual(NewName, updatedPage.Name); + } + + [Test] + public async Task Can_Get_Updated_Draft_Content_By_Key() + { + // Arrange + Textpage.InvariantName = NewName; + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = NewName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var updatedPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(NewName, updatedPage.Name); + } + + [Test] + [TestCase(true, true)] + [TestCase(false, false)] + // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. + public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + Textpage.InvariantName = NewName; + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = NewName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + + // Assert + Assert.AreEqual(result, NewName.Equals(textPage.Name)); + } + + [Test] + [TestCase(true, true)] + [TestCase(false, false)] + // BETTER NAMING, CURRENTLY THIS IS TESTING BOTH THE PUBLISHED AND THE DRAFT OF THE PUBLISHED. + public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + Textpage.InvariantName = NewName; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = NewName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + + // Assert + Assert.AreEqual(result, NewName.Equals(textPage.Name)); + } + + [Test] + public async Task Can_Get_Draft_Content_Property_By_Id() + { + // Arrange + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Draft_Content_Property_By_Key() + { + // Arrange + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Published_Content_Property_By_Id() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Published_Content_Property_By_Key() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key() + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(titleValue, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Updated_Draft_Content_Property_By_Id() + { + // Arrange + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + + // Assert + Assert.AreEqual(NewTitle, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Updated_Draft_Content_Property_By_Key() + { + // Arrange + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(NewTitle, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Updated_Published_Content_Property_By_Id() + { + // Arrange + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(NewTitle, textPage.Value("title")); + } + + [Test] + public async Task Can_Get_Updated_Published_Content_Property_By_Key() + { + // Arrange + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + + // Assert + Assert.AreEqual(NewTitle, textPage.Value("title")); + } + + [Test] + [TestCase(true, "New Title")] + [TestCase(false, "Welcome to our Home page")] + public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + + // Assert + Assert.AreEqual(titleName, textPage.Value("title")); + } + + [Test] + [TestCase(true, "New Name")] + [TestCase(false, "Welcome to our Home page")] + public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + Textpage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; + + ContentUpdateModel updateModel = new ContentUpdateModel + { + InvariantName = Textpage.InvariantName, + InvariantProperties = Textpage.InvariantProperties, + Variants = Textpage.Variants, + TemplateKey = Textpage.TemplateKey, + }; + + await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + + // Assert + Assert.AreEqual(titleName, textPage.Value("title")); + } + + [Test] + public async Task Can_Not_Get_Deleted_Content_By_Id() + { + // Arrange + var content = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + Assert.IsNotNull(content); + await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + + // Assert + Assert.IsNull(textPage); + } + + [Test] + public async Task Can_Not_Get_Deleted_Content_By_Key() + { + // Arrange + await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); + var result = await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); + + // Assert + Assert.IsNull(textPage); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + + // Assert + Assert.IsNull(textPage); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) + { + // Arrange + await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + + // Assert + Assert.IsNull(textPage); + } + + private void AssertTextPage(IPublishedContent textPage) + { + Assert.Multiple(() => + { + Assert.IsNotNull(textPage); + Assert.AreEqual(Textpage.Key, textPage.Key); + Assert.AreEqual(Textpage.ContentTypeKey, textPage.ContentType.Key); + Assert.AreEqual(Textpage.InvariantName, textPage.Name); + }); + + AssertProperties(Textpage.InvariantProperties, textPage.Properties); + } + + private void AssertProperties(IEnumerable propertyCollection, IEnumerable publishedProperties) + { + foreach (var prop in propertyCollection) + { + AssertProperty(prop, publishedProperties.First(x => x.Alias == prop.Alias)); + } + } + + private void AssertProperty(PropertyValueModel property, IPublishedProperty publishedProperty) + { + Assert.Multiple(() => + { + Assert.AreEqual(property.Alias, publishedProperty.Alias); + Assert.AreEqual(property.Value, publishedProperty.GetSourceValue()); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs new file mode 100644 index 0000000000..8f06be20c3 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs @@ -0,0 +1,193 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest +{ + private string _englishIsoCode = "en-US"; + private string _danishIsoCode = "da-DK"; + private string _variantTitleAlias = "variantTitle"; + private string _variantTitleName = "Variant Title"; + private string _invariantTitleAlias = "invariantTitle"; + private string _invariantTitleName = "Invariant Title"; + + private IContentTypeService ContentTypeService => GetRequiredService(); + + private ILanguageService LanguageService => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IContent VariantPage { get; set; } + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + [SetUp] + public async Task Setup() => await CreateTestData(); + + [Test] + public async Task Can_Set_Invariant_Title() + { + // Arrange + await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true); + var updatedInvariantTitle = "Updated Invariant Title"; + var updatedVariantTitle = "Updated Variant Title"; + + + var updateModel = new ContentUpdateModel + { + InvariantProperties = new[] + { + new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } + }, + Variants = new [] + { + new VariantModel + { + Culture = _englishIsoCode, + Name = "Updated English Name", + Properties = new [] + { + new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } + } + }, + new VariantModel + { + Culture = _danishIsoCode, + Name = "Updated Danish Name", + Properties = new [] + { + new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } + }, + }, + }, + }; + + var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true); + + // Assert + using var contextReference = UmbracoContextFactory.EnsureUmbracoContext(); + Assert.AreEqual(updatedInvariantTitle, textPage.Value(_invariantTitleAlias, "", "")); + Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _englishIsoCode)); + Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _danishIsoCode)); + } + + [Test] + public async Task Can_Set_Invariant_Title_On_One_Culture() + { + // Arrange + await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true); + var updatedInvariantTitle = "Updated Invariant Title"; + var updatedVariantTitle = "Updated Invariant Title"; + + + var updateModel = new ContentUpdateModel + { + InvariantProperties = new[] + { + new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } + }, + Variants = new [] + { + new VariantModel + { + Culture = _englishIsoCode, + Name = "Updated English Name", + Properties = new [] + { + new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } + } + }, + }, + }; + + var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Act + var textPage = await PublishedContentHybridCache.GetByIdAsync(VariantPage.Id, true); + + // Assert + using var contextReference = UmbracoContextFactory.EnsureUmbracoContext(); + Assert.AreEqual(updatedInvariantTitle, textPage.Value(_invariantTitleAlias, "", "")); + Assert.AreEqual(updatedVariantTitle, textPage.Value(_variantTitleAlias, _englishIsoCode)); + Assert.AreEqual(_variantTitleName, textPage.Value(_variantTitleAlias, _danishIsoCode)); + } + + + private async Task CreateTestData() + { + // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. + var language = new LanguageBuilder() + .WithCultureInfo(_danishIsoCode) + .Build(); + + await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey); + + var contentType = new ContentTypeBuilder() + .WithAlias("cultureVariationTest") + .WithName("Culture Variation Test") + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias(_variantTitleAlias) + .WithName(_variantTitleName) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias(_invariantTitleAlias) + .WithName(_invariantTitleName) + .WithVariations(ContentVariation.Nothing) + .Done() + .Build(); + contentType.AllowedAsRoot = true; + ContentTypeService.Save(contentType); + var rootContentCreateModel = new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = new[] + { + new VariantModel + { + Culture = "en-US", + Name = "English Page", + Properties = new [] + { + new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } + }, + }, + new VariantModel + { + Culture = "da-DK", + Name = "Danish Page", + Properties = new [] + { + new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } + }, + }, + }, + }; + + var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); + VariantPage = result.Result.Content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs new file mode 100644 index 0000000000..63fc6eb841 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MediaHybridCacheTests.cs @@ -0,0 +1,239 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class MediaHybridCacheTests : UmbracoIntegrationTest +{ + private IPublishedMediaCache PublishedMediaHybridCache => GetRequiredService(); + + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private IMediaEditingService MediaEditingService => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + // TODO: make test with MediaWithCrops + + [Test] + public async Task Can_Get_Media_By_Key() + { + // Arrange + var newMediaType = new MediaTypeBuilder() + .WithAlias("album") + .WithName("Album") + .Build(); + + newMediaType.AllowedAsRoot = true; + MediaTypeService.Save(newMediaType); + + var createModel = new MediaCreateModel + { + ContentTypeKey = newMediaType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Image", + }; + + var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Act + var media = await PublishedMediaHybridCache.GetByKeyAsync(result.Result.Content.Key); + + // Assert + Assert.IsNotNull(media); + Assert.AreEqual("Image", media.Name); + Assert.AreEqual(newMediaType.Key, media.ContentType.Key); + } + + [Test] + public async Task Can_Get_Media_By_Id() + { + // Arrange + var newMediaType = new MediaTypeBuilder() + .WithAlias("album") + .WithName("Album") + .Build(); + + newMediaType.AllowedAsRoot = true; + MediaTypeService.Save(newMediaType); + + var createModel = new MediaCreateModel + { + ContentTypeKey = newMediaType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Image", + }; + + var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Act + var media = await PublishedMediaHybridCache.GetByIdAsync(result.Result.Content.Id); + + // Assert + Assert.IsNotNull(media); + Assert.AreEqual("Image", media.Name); + Assert.AreEqual(newMediaType.Key, media.ContentType.Key); + } + + [Test] + public async Task Cannot_Get_Non_Existing_Media_By_Key() + { + // Act + var media = await PublishedMediaHybridCache.GetByKeyAsync(Guid.NewGuid()); + + // Assert + Assert.IsNull(media); + } + + [Test] + public async Task Cannot_Get_Non_Existing_Media_By_Id() + { + // Act + var media = await PublishedMediaHybridCache.GetByIdAsync(124214); + + // Assert + Assert.IsNull(media); + } + + [Test] + public async Task Can_Get_Media_Property_By_Key() + { + // Arrange + var media = await CreateMedia(); + + // Act + var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key); + + UmbracoContextFactory.EnsureUmbracoContext(); + + // Assert + Assert.IsNotNull(media); + Assert.AreEqual("Image", media.Name); + Assert.AreEqual("NewTitle", publishedMedia.Value("title")); + } + + [Test] + public async Task Can_Get_Media_Property_By_Id() + { + // Arrange + var media = await CreateMedia(); + + // Act + var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key); + + UmbracoContextFactory.EnsureUmbracoContext(); + + // Assert + Assert.IsNotNull(publishedMedia); + Assert.AreEqual("Image", publishedMedia.Name); + Assert.AreEqual("NewTitle", publishedMedia.Value("title")); + } + + [Test] + public async Task Can_Get_Updated_Media() + { + // Arrange + var media = await CreateMedia(); + await PublishedMediaHybridCache.GetByIdAsync(media.Id); + + // Act + var updateModel = new MediaUpdateModel() + { + InvariantName = "Update name", + InvariantProperties = new List() + { + new() + { + Alias = "title", + Value = "Updated Title" + } + } + }; + + var updateAttempt = await MediaEditingService.UpdateAsync(media.Key, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updateAttempt.Success); + var publishedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id); + UmbracoContextFactory.EnsureUmbracoContext(); + + // Assert + Assert.IsNotNull(publishedMedia); + Assert.AreEqual("Update name", publishedMedia.Name); + Assert.AreEqual("Updated Title", publishedMedia.Value("title")); + } + + [Test] + public async Task Cannot_Get_Deleted_Media_By_Id() + { + // Arrange + var media = await CreateMedia(); + var publishedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id); + Assert.IsNotNull(publishedMedia); + + await MediaEditingService.DeleteAsync(media.Key, Constants.Security.SuperUserKey); + + // Act + var deletedMedia = await PublishedMediaHybridCache.GetByIdAsync(media.Id); + + // Assert + Assert.IsNull(deletedMedia); + } + + [Test] + public async Task Cannot_Get_Deleted_Media_By_Key() + { + // Arrange + var media = await CreateMedia(); + var publishedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key); + Assert.IsNotNull(publishedMedia); + + await MediaEditingService.DeleteAsync(media.Key, Constants.Security.SuperUserKey); + + // Act + var deletedMedia = await PublishedMediaHybridCache.GetByKeyAsync(media.Key); + + // Assert + Assert.IsNull(deletedMedia); + } + + private async Task CreateMedia() + { + IMediaType mediaType = MediaTypeBuilder.CreateSimpleMediaType("test", "Test"); + mediaType.AllowedAsRoot = true; + MediaTypeService.Save(mediaType); + + var createModel = new MediaCreateModel + { + ContentTypeKey = mediaType.Key, + ParentKey = Constants.System.RootKey, + InvariantName = "Image", + InvariantProperties = new List() + { + new() + { + Alias = "title", + Value = "NewTitle" + } + } + }; + + var result = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + return result.Result.Content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs new file mode 100644 index 0000000000..9f1c201deb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/MemberHybridCacheTests.cs @@ -0,0 +1,81 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class MemberHybridCacheTests : UmbracoIntegrationTest +{ + private IPublishedMemberCache PublishedMemberHybridCache => GetRequiredService(); + + private IMemberEditingService MemberEditingService => GetRequiredService(); + + private IMemberService MemberService => GetRequiredService(); + + private IMemberTypeService MemberTypeService => GetRequiredService(); + + private IMemberGroupService MemberGroupService => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + [Test] + public async Task Can_Get_Member_By_Key() + { + Guid key = Guid.NewGuid(); + var createdMember = await CreateMemberAsync(key); + + // Act + var member = await PublishedMemberHybridCache.GetAsync(createdMember); + + // Assert + Assert.IsNotNull(member); + Assert.AreEqual("The title value", member.Value("title")); + Assert.AreEqual("test@test.com", member.Email); + Assert.AreEqual("test", member.UserName); + Assert.IsTrue(member.IsApproved); + Assert.AreEqual("T. Est", member.Name); + } + + private async Task CreateMemberAsync(Guid? key = null, bool titleIsSensitive = false) + { + IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType(); + memberType.SetIsSensitiveProperty("title", titleIsSensitive); + MemberTypeService.Save(memberType); + MemberService.AddRole("RoleOne"); + var group = MemberGroupService.GetByName("RoleOne"); + + var createModel = new MemberCreateModel + { + Key = key, + Email = "test@test.com", + Username = "test", + Password = "SuperSecret123", + IsApproved = true, + ContentTypeKey = memberType.Key, + Roles = new [] { group.Key }, + InvariantName = "T. Est", + InvariantProperties = new[] + { + new PropertyValueModel { Alias = "title", Value = "The title value" }, + new PropertyValueModel { Alias = "author", Value = "The author value" } + } + }; + + var result = await MemberEditingService.CreateAsync(createModel, SuperUser()); + Assert.IsTrue(result.Success); + return result.Result.Content; + } + + private IUser SuperUser() => GetRequiredService().GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult(); + +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index de8274a94f..830f322530 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 462205b231..679daeca75 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Web.BackOffice.UrlAndDomains; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] public class DomainAndUrlsTests : UmbracoIntegrationTest { [SetUp] @@ -66,6 +67,7 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest protected override void CustomTestSetup(IUmbracoBuilder builder) { builder.Services.AddUnique(_variationContextAccessor); + builder.AddUmbracoHybridCache(); builder.AddNuCache(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs index 9e34979c6e..06e22d935d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentPickerValueConverterTests.cs @@ -15,7 +15,7 @@ public class ContentPickerValueConverterTests : PropertyValueConverterTests { private ContentPickerValueConverter CreateValueConverter(IApiContentNameProvider? nameProvider = null) => new ContentPickerValueConverter( - PublishedSnapshotAccessor, + PublishedContentCacheMock.Object, new ApiContentBuilder( nameProvider ?? new ApiContentNameProvider(), CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs index 84ee7b0841..081afae7ec 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MarkdownEditorValueConverterTests.cs @@ -10,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Templates; -using Umbraco.Cms.Core.Web; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -24,7 +23,7 @@ public class MarkdownEditorValueConverterTests : PropertyValueConverterTests [TestCase(123, "")] public void MarkdownEditorValueConverter_ConvertsValueToMarkdownString(object inter, string expected) { - var linkParser = new HtmlLocalLinkParser(Mock.Of(), Mock.Of()); + var linkParser = new HtmlLocalLinkParser(Mock.Of()); var urlParser = new HtmlUrlParser(Mock.Of>(), Mock.Of>(), Mock.Of(), Mock.Of()); var valueConverter = new MarkdownEditorValueConverter(linkParser, urlParser); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs index 2f1b05abf7..75a331bd31 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/OutputExpansionStrategyTestBase.cs @@ -375,7 +375,7 @@ public abstract class OutputExpansionStrategyTestBase : PropertyValueConverterTe internal PublishedElementPropertyBase CreateContentPickerProperty(IPublishedElement parent, Guid pickedContentKey, string propertyTypeAlias, IApiContentBuilder contentBuilder) { - ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedSnapshotAccessor, contentBuilder); + ContentPickerValueConverter contentPickerValueConverter = new ContentPickerValueConverter(PublishedContentCacheMock.Object, contentBuilder); var contentPickerPropertyType = SetupPublishedPropertyType(contentPickerValueConverter, propertyTypeAlias, Constants.PropertyEditors.Aliases.ContentPicker); return new PublishedElementPropertyBase(contentPickerPropertyType, parent, false, PropertyCacheLevel.None, new GuidUdi(Constants.UdiEntityType.Document, pickedContentKey).ToString()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index 6ef0f74e2f..67faeaf7ba 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -29,9 +29,8 @@ public class HtmlLocalLinkParserTests

media

"; - - var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var parser = new HtmlLocalLinkParser(umbracoContextAccessor, Mock.Of()); + + var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); @@ -56,8 +55,7 @@ public class HtmlLocalLinkParserTests hello

"; - var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var parser = new HtmlLocalLinkParser(umbracoContextAccessor, Mock.Of()); + var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); @@ -90,8 +88,7 @@ public class HtmlLocalLinkParserTests media

"; - var umbracoContextAccessor = new TestUmbracoContextAccessor(); - var parser = new HtmlLocalLinkParser(umbracoContextAccessor, Mock.Of()); + var parser = new HtmlLocalLinkParser(Mock.Of()); var result = parser.FindUdisFromLocalLinks(input).ToList(); @@ -204,7 +201,7 @@ public class HtmlLocalLinkParserTests mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); mediaCache.Setup(x => x.GetById(It.IsAny())).Returns(media.Object); - var linkParser = new HtmlLocalLinkParser(umbracoContextAccessor, publishedUrlProvider); + var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); var output = linkParser.EnsureInternalLinks(input); diff --git a/umbraco.sln b/umbraco.sln index 5e26e18d8c..e194c7f6f5 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -188,6 +188,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umbraco.Tests.AcceptanceTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Cms.Api.Management", "src\Umbraco.Cms.Api.Management\Umbraco.Cms.Api.Management.csproj", "{B4929148-3BD9-4589-829D-7C31FFCFF6D7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.PublishedCache.HybridCache", "src\Umbraco.PublishedCache.HybridCache\Umbraco.PublishedCache.HybridCache.csproj", "{CB0B9817-EDBC-4D6D-B4D2-969019C4606D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -362,6 +364,12 @@ Global {B4929148-3BD9-4589-829D-7C31FFCFF6D7}.Release|Any CPU.Build.0 = Release|Any CPU {B4929148-3BD9-4589-829D-7C31FFCFF6D7}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU {B4929148-3BD9-4589-829D-7C31FFCFF6D7}.SkipTests|Any CPU.Build.0 = Debug|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.Release|Any CPU.Build.0 = Release|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.SkipTests|Any CPU.ActiveCfg = Debug|Any CPU + {CB0B9817-EDBC-4D6D-B4D2-969019C4606D}.SkipTests|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From f12aafd5bebeee8de9b64a5f7566bea98d5806ec Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 13 Sep 2024 10:42:19 +0200 Subject: [PATCH 13/38] Updated to dotnet 9 RC.1 - and other nuget packages (#17053) * Updated nuget packages and dotnet 9 * attempt to make pipelines happy * Another attempt to make pipelines happy --- Directory.Packages.props | 76 +++++++++---------- global.json | 2 +- .../PartialViewViewModelsMapDefinition.cs | 2 +- .../Script/ScriptViewModelsMapDefinition.cs | 2 +- .../StylesheetviewModelsMapDefinition.cs | 2 +- tests/Directory.Packages.props | 16 ++-- .../PublishedContentQueryTests.cs | 4 +- 7 files changed, 51 insertions(+), 53 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5f2a028c77..b00c78d638 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,35 +5,35 @@ - + - + - - - - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -48,7 +48,7 @@ - + @@ -59,24 +59,24 @@ - - - - + + + + - - - + + + - - + + - - - + + + - + @@ -85,10 +85,10 @@ - + - + - + \ No newline at end of file diff --git a/global.json b/global.json index a718288b1a..9c2a135743 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100-preview.7.24407.12", + "version": "9.0.100-rc.1.24452.12", "rollForward": "latestFeature", "allowPrerelease": true } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs index 97e241feff..53c1efeb3f 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/PartialView/PartialViewViewModelsMapDefinition.cs @@ -64,7 +64,7 @@ public class PartialViewViewModelsMapDefinition : IMapDefinition { target.Path = source.Path.SystemPathToVirtualPath(); target.Name = source.Name; - target.Parent = source.ParentPath.IsNullOrEmpty() + target.Parent = string.IsNullOrEmpty(source.ParentPath) ? null : new FileSystemFolderModel { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs index 75096091c3..dae8bcf177 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Script/ScriptViewModelsMapDefinition.cs @@ -59,7 +59,7 @@ public class ScriptViewModelsMapDefinition : IMapDefinition { target.Path = source.Path.SystemPathToVirtualPath(); target.Name = source.Name; - target.Parent = source.ParentPath.IsNullOrEmpty() + target.Parent = string.IsNullOrEmpty(source.ParentPath) ? null : new FileSystemFolderModel { diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs index 30aa1651ec..65c1fe0be9 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Stylesheet/StylesheetviewModelsMapDefinition.cs @@ -59,7 +59,7 @@ public class StylesheetViewModelsMapDefinition : IMapDefinition { target.Path = source.Path.SystemPathToVirtualPath(); target.Name = source.Name; - target.Parent = source.ParentPath.IsNullOrEmpty() + target.Parent = string.IsNullOrEmpty(source.ParentPath) ? null : new FileSystemFolderModel { diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index c8e77d9603..629acca752 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -5,25 +5,23 @@ - - - + + + - - + + - - - + - + \ No newline at end of file diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs index 08e4c20343..093e446adf 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/PublishedContentQueryTests.cs @@ -59,8 +59,8 @@ public class PublishedContentQueryTests : ExamineBaseTest new Dictionary { [name] = "Hello world, there are products here", - [UmbracoExamineFieldNames.VariesByCultureFieldName] = culture.IsNullOrEmpty() ? "n" : "y", - [culture.IsNullOrEmpty() ? UmbracoExamineFieldNames.PublishedFieldName : $"{UmbracoExamineFieldNames.PublishedFieldName}_{culture}"] = "y" + [UmbracoExamineFieldNames.VariesByCultureFieldName] = string.IsNullOrEmpty(culture) ? "n" : "y", + [string.IsNullOrEmpty(culture) ? UmbracoExamineFieldNames.PublishedFieldName : $"{UmbracoExamineFieldNames.PublishedFieldName}_{culture}"] = "y" })); } } From 6dae5024de0cb271f3c3acc02d3bfd095c0e5037 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 16 Sep 2024 10:30:19 +0200 Subject: [PATCH 14/38] Add EditorAlias to content value outputs (#17032) --- .../ContentCollectionControllerBase.cs | 2 +- .../DocumentCollectionControllerBase.cs | 2 +- .../MediaCollectionControllerBase.cs | 2 +- .../Mapping/Content/ContentMapDefinition.cs | 5 +- .../Mapping/Document/DocumentMapDefinition.cs | 2 +- .../Document/DocumentVersionMapDefinition.cs | 2 +- .../Mapping/Media/MediaMapDefinition.cs | 2 +- .../Mapping/Member/MemberMapDefinition.cs | 2 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 101 ++++++++++++++++-- .../ContentCollectionResponseModelBase.cs | 2 +- .../DocumentCollectionResponseModel.cs | 2 +- .../Document/DocumentResponseModel.cs | 2 +- .../Document/DocumentValueResponseModel.cs | 7 ++ .../Document/DocumentVersionResponseModel.cs | 2 +- .../DocumentBlueprintResponseModel.cs | 2 +- .../MediaCollectionResponseModel.cs | 2 +- .../ViewModels/Media/MediaResponseModel.cs | 2 +- .../Media/MediaValueResponseModel.cs | 7 ++ .../ViewModels/Member/MemberResponseModel.cs | 2 +- .../Member/MemberValueResponseModel.cs | 7 ++ .../ContentEditing/ValueResponseModelBase.cs | 9 ++ 21 files changed, 142 insertions(+), 24 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs create mode 100644 src/Umbraco.Core/Models/ContentEditing/ValueResponseModelBase.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs index 95f4bb5498..8012da4669 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Content/ContentCollectionControllerBase.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Content; public abstract class ContentCollectionControllerBase : ManagementApiControllerBase where TContent : class, IContentBase where TCollectionResponseModel : ContentResponseModelBase - where TValueResponseModelBase : ValueModelBase + where TValueResponseModelBase : ValueResponseModelBase where TVariantResponseModel : VariantResponseModelBase { private readonly IUmbracoMapper _mapper; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs index 71dfc75fb1..b4ac5a86da 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Collection/DocumentCollectionControllerBase.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Document.Collection; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Document}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Document))] [Authorize(Policy = AuthorizationPolicies.TreeAccessDocuments)] -public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase +public abstract class DocumentCollectionControllerBase : ContentCollectionControllerBase { protected DocumentCollectionControllerBase(IUmbracoMapper mapper) : base(mapper) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs index c6bf7eadda..cf6b05d8b9 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Collection/MediaCollectionControllerBase.cs @@ -15,7 +15,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.Media.Collection; [VersionedApiBackOfficeRoute($"{Constants.Web.RoutePath.Collection}/{Constants.UdiEntityType.Media}")] [ApiExplorerSettings(GroupName = nameof(Constants.UdiEntityType.Media))] [Authorize(Policy = AuthorizationPolicies.SectionAccessMedia)] -public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase +public abstract class MediaCollectionControllerBase : ContentCollectionControllerBase { protected MediaCollectionControllerBase(IUmbracoMapper mapper) : base(mapper) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs index bba23ea98d..038e3fbf8b 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Content/ContentMapDefinition.cs @@ -8,7 +8,7 @@ namespace Umbraco.Cms.Api.Management.Mapping.Content; public abstract class ContentMapDefinition where TContent : IContentBase - where TValueViewModel : ValueModelBase, new() + where TValueViewModel : ValueResponseModelBase, new() where TVariantViewModel : VariantResponseModelBase, new() { private readonly PropertyEditorCollection _propertyEditorCollection; @@ -36,7 +36,8 @@ public abstract class ContentMapDefinition, IMapDefinition +public class DocumentMapDefinition : ContentMapDefinition, IMapDefinition { private readonly CommonMapper _commonMapper; diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs index 40359b08c5..5e12e245bd 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentVersionMapDefinition.cs @@ -9,7 +9,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Mapping.Document; -public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition +public class DocumentVersionMapDefinition : ContentMapDefinition, IMapDefinition { public DocumentVersionMapDefinition(PropertyEditorCollection propertyEditorCollection) : base(propertyEditorCollection) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs index d4760abe2d..df1743d235 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Media/MediaMapDefinition.cs @@ -10,7 +10,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Mapping.Media; -public class MediaMapDefinition : ContentMapDefinition, IMapDefinition +public class MediaMapDefinition : ContentMapDefinition, IMapDefinition { private readonly CommonMapper _commonMapper; diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs index a6e16c8c7b..fedc8e4eca 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Member/MemberMapDefinition.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Api.Management.Mapping.Member; -public class MemberMapDefinition : ContentMapDefinition, IMapDefinition +public class MemberMapDefinition : ContentMapDefinition, IMapDefinition { public MemberMapDefinition(PropertyEditorCollection propertyEditorCollection) : base(propertyEditorCollection) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index bdef772cfe..65319682e3 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -36349,7 +36349,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/DocumentValueModel" + "$ref": "#/components/schemas/DocumentValueResponseModel" } ] } @@ -36434,7 +36434,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/DocumentValueModel" + "$ref": "#/components/schemas/DocumentValueResponseModel" } ] } @@ -36687,7 +36687,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/DocumentValueModel" + "$ref": "#/components/schemas/DocumentValueResponseModel" } ] } @@ -37369,6 +37369,35 @@ }, "additionalProperties": false }, + "DocumentValueResponseModel": { + "required": [ + "alias", + "editorAlias" + ], + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "segment": { + "type": "string", + "nullable": true + }, + "alias": { + "minLength": 1, + "type": "string" + }, + "value": { + "nullable": true + }, + "editorAlias": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "DocumentVariantItemResponseModel": { "required": [ "name", @@ -37527,7 +37556,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/DocumentValueModel" + "$ref": "#/components/schemas/DocumentValueResponseModel" } ] } @@ -38569,7 +38598,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/MediaValueModel" + "$ref": "#/components/schemas/MediaValueResponseModel" } ] } @@ -38752,7 +38781,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/MediaValueModel" + "$ref": "#/components/schemas/MediaValueResponseModel" } ] } @@ -39340,6 +39369,35 @@ }, "additionalProperties": false }, + "MediaValueResponseModel": { + "required": [ + "alias", + "editorAlias" + ], + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "segment": { + "type": "string", + "nullable": true + }, + "alias": { + "minLength": 1, + "type": "string" + }, + "value": { + "nullable": true + }, + "editorAlias": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "MediaVariantRequestModel": { "required": [ "name" @@ -39508,7 +39566,7 @@ "items": { "oneOf": [ { - "$ref": "#/components/schemas/MemberValueModel" + "$ref": "#/components/schemas/MemberValueResponseModel" } ] } @@ -39993,6 +40051,35 @@ }, "additionalProperties": false }, + "MemberValueResponseModel": { + "required": [ + "alias", + "editorAlias" + ], + "type": "object", + "properties": { + "culture": { + "type": "string", + "nullable": true + }, + "segment": { + "type": "string", + "nullable": true + }, + "alias": { + "minLength": 1, + "type": "string" + }, + "value": { + "nullable": true + }, + "editorAlias": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "MemberVariantRequestModel": { "required": [ "name" diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs index 735e448fb2..2426abbecd 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Content/ContentCollectionResponseModelBase.cs @@ -4,7 +4,7 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Content; public abstract class ContentCollectionResponseModelBase : ContentResponseModelBase - where TValueResponseModelBase : ValueModelBase + where TValueResponseModelBase : ValueResponseModelBase where TVariantResponseModel : VariantResponseModelBase { public string? Creator { get; set; } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs index 453f2ee72a..b67f4088ce 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.DocumentType; namespace Umbraco.Cms.Api.Management.ViewModels.Document.Collection; -public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase +public class DocumentCollectionResponseModel : ContentCollectionResponseModelBase { public DocumentTypeCollectionReferenceResponseModel DocumentType { get; set; } = new(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs index d2eeb2e6af..2854a53320 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentResponseModel : DocumentResponseModelBase +public class DocumentResponseModel : DocumentResponseModelBase { public IEnumerable Urls { get; set; } = Enumerable.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs new file mode 100644 index 0000000000..c112da394c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentValueResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class DocumentValueResponseModel : ValueResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs index 486bee306f..5068ab3f40 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentVersionResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.Document; -public class DocumentVersionResponseModel : DocumentResponseModelBase +public class DocumentVersionResponseModel : DocumentResponseModelBase { public ReferenceByIdModel? Document { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs index e23cb10f41..7b8a60b6e2 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DocumentBlueprint/DocumentBlueprintResponseModel.cs @@ -2,6 +2,6 @@ using Umbraco.Cms.Api.Management.ViewModels.Document; namespace Umbraco.Cms.Api.Management.ViewModels.DocumentBlueprint; -public class DocumentBlueprintResponseModel : DocumentResponseModelBase +public class DocumentBlueprintResponseModel : DocumentResponseModelBase { } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs index fb772f858f..72891a9bd1 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/Collection/MediaCollectionResponseModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.MediaType; namespace Umbraco.Cms.Api.Management.ViewModels.Media.Collection; -public class MediaCollectionResponseModel : ContentCollectionResponseModelBase +public class MediaCollectionResponseModel : ContentCollectionResponseModelBase { public MediaTypeCollectionReferenceResponseModel MediaType { get; set; } = new(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs index dc6bc6da40..8b78b98f2c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaResponseModel.cs @@ -3,7 +3,7 @@ using Umbraco.Cms.Api.Management.ViewModels.MediaType; namespace Umbraco.Cms.Api.Management.ViewModels.Media; -public class MediaResponseModel : ContentResponseModelBase +public class MediaResponseModel : ContentResponseModelBase { public IEnumerable Urls { get; set; } = Enumerable.Empty(); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs new file mode 100644 index 0000000000..10ffd04bd0 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Media/MediaValueResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Api.Management.ViewModels.Media; + +public class MediaValueResponseModel : ValueResponseModelBase +{ +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs index df5f9c9a22..626d5e6b8e 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberResponseModel.cs @@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Models.Membership; namespace Umbraco.Cms.Api.Management.ViewModels.Member; -public class MemberResponseModel : ContentResponseModelBase +public class MemberResponseModel : ContentResponseModelBase { public string Email { get; set; } = string.Empty; diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs new file mode 100644 index 0000000000..1db8190ecb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Member/MemberValueResponseModel.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Api.Management.ViewModels.Member; + +public class MemberValueResponseModel : ValueResponseModelBase +{ +} diff --git a/src/Umbraco.Core/Models/ContentEditing/ValueResponseModelBase.cs b/src/Umbraco.Core/Models/ContentEditing/ValueResponseModelBase.cs new file mode 100644 index 0000000000..65e61cfaba --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ValueResponseModelBase.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public abstract class ValueResponseModelBase : ValueModelBase +{ + [Required] + public string EditorAlias { get; set; } = string.Empty; +} From e9f75f01217312e90f46a59495af546c2467a34d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:27:06 +0200 Subject: [PATCH 15/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 47c3feeb6d..c28d5638e8 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 47c3feeb6d7245c13a430c68b0238939a81dd36d +Subproject commit c28d5638e80cdb7c1d9306cd3ee97d5fc02f275e From 19c571134f21d28074ee12bf99c5c17575d34c46 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 17 Sep 2024 10:58:47 +0200 Subject: [PATCH 16/38] Add notification alias to document notifications endpoint output (#17028) --- .../DocumentNotificationPresentationFactory.cs | 15 ++++++++------- src/Umbraco.Cms.Api.Management/OpenApi.json | 4 ++++ .../DocumentNotificationsResponseModel.cs | 2 ++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs index 549163df91..7daf762b78 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentNotificationPresentationFactory.cs @@ -27,13 +27,14 @@ internal sealed class DocumentNotificationPresentationFactory : IDocumentNotific .ToArray() ?? Array.Empty(); - var availableActionIds = _actionCollection.Where(a => a.ShowInNotifier).Select(a => a.Letter.ToString()).ToArray(); - - return await Task.FromResult( - availableActionIds.Select(actionId => new DocumentNotificationResponseModel + return await Task.FromResult(_actionCollection + .Where(action => action.ShowInNotifier) + .Select(action => new DocumentNotificationResponseModel { - ActionId = actionId, - Subscribed = subscribedActionIds.Contains(actionId) - }).ToArray()); + ActionId = action.Letter, + Alias = action.Alias, + Subscribed = subscribedActionIds.Contains(action.Letter) + }) + .ToArray()); } } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 65319682e3..80068de047 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -36551,6 +36551,7 @@ "DocumentNotificationResponseModel": { "required": [ "actionId", + "alias", "subscribed" ], "type": "object", @@ -36558,6 +36559,9 @@ "actionId": { "type": "string" }, + "alias": { + "type": "string" + }, "subscribed": { "type": "boolean" } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs index b3ae3b8f6e..cf7665a643 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentNotificationsResponseModel.cs @@ -4,5 +4,7 @@ public class DocumentNotificationResponseModel { public required string ActionId { get; set; } + public required string Alias { get; set; } + public required bool Subscribed { get; set; } } From bb3ee49a2b8f3e19834cd625106b4f1cfee8a9bf Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:58:36 +0200 Subject: [PATCH 17/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index c28d5638e8..cd1c09855d 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit c28d5638e80cdb7c1d9306cd3ee97d5fc02f275e +Subproject commit cd1c09855dbbe9719aea104bffb9d117e31a3fe7 From f19e0feebd954d0e5e296a6939923ad3c29f46dd Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 17 Sep 2024 20:45:56 +0200 Subject: [PATCH 18/38] Post merge review fixes --- Directory.Packages.props | 5 ++--- tests/Directory.Packages.props | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b00c78d638..69633196fb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,11 +20,10 @@ - - + @@ -91,4 +90,4 @@ - \ No newline at end of file + diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 629acca752..a3b4ceff47 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -11,8 +11,6 @@ - - @@ -24,4 +22,4 @@ - \ No newline at end of file + From 64cfac062b9e869f0e919bfeff8af8ceb4a4785d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:26:17 +0200 Subject: [PATCH 19/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index cd1c09855d..360c3ae9dc 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit cd1c09855dbbe9719aea104bffb9d117e31a3fe7 +Subproject commit 360c3ae9dc79918c635522cf897c0d7c3e6982af From dcfd29f32373a875d323dd526be6307b24fed2eb Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:35:23 +0200 Subject: [PATCH 20/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 360c3ae9dc..c684916457 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 360c3ae9dc79918c635522cf897c0d7c3e6982af +Subproject commit c684916457bd1e636b7cd6466383a2eeae8281f3 From 2270db6efccbe1e0157e4ab248ee6d9d507a91f6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:41:36 +0200 Subject: [PATCH 21/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index c684916457..47359b5de8 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit c684916457bd1e636b7cd6466383a2eeae8281f3 +Subproject commit 47359b5de85db162c3210783e9bbe002e1e4aec3 From cf6137db1869020453d410c1307060af4503fc89 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Mon, 23 Sep 2024 09:45:46 +0200 Subject: [PATCH 22/38] Add `IAsyncComponent` to allow async initialize/terminate (#16536) * Add IAsyncComponent * Rewrite to use IAsyncComposer * Add AsyncComponentBase and RuntimeAsyncComponentBase * Remove manual disposing of components on restart --- .../Composing/AsyncComponentBase.cs | 55 +++++++++++++++++++ .../Composing/ComponentCollection.cs | 47 ++++++++-------- .../Composing/ComponentCollectionBuilder.cs | 22 ++++---- .../Composing/ComponentComposer.cs | 23 +++++--- src/Umbraco.Core/Composing/IAsyncComponent.cs | 35 ++++++++++++ src/Umbraco.Core/Composing/IComponent.cs | 38 +++++++------ .../Composing/RuntimeAsyncComponentBase.cs | 23 ++++++++ .../UmbracoBuilder.CollectionBuilders.cs | 2 +- .../Extensions/ObjectExtensions.cs | 3 +- .../UmbracoApplicationStartingNotification.cs | 30 +++++----- .../UmbracoApplicationStoppingNotification.cs | 19 +++---- .../Runtime/CoreRuntime.cs | 4 +- .../Umbraco.Core/Components/ComponentTests.cs | 20 ++++--- 13 files changed, 221 insertions(+), 100 deletions(-) create mode 100644 src/Umbraco.Core/Composing/AsyncComponentBase.cs create mode 100644 src/Umbraco.Core/Composing/IAsyncComponent.cs create mode 100644 src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs diff --git a/src/Umbraco.Core/Composing/AsyncComponentBase.cs b/src/Umbraco.Core/Composing/AsyncComponentBase.cs new file mode 100644 index 0000000000..5ba3dd168d --- /dev/null +++ b/src/Umbraco.Core/Composing/AsyncComponentBase.cs @@ -0,0 +1,55 @@ +namespace Umbraco.Cms.Core.Composing; + +/// +/// +/// By default, the component will not execute if Umbraco is restarting. +/// +public abstract class AsyncComponentBase : IAsyncComponent +{ + /// + public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken) + { + if (CanExecute(isRestarting)) + { + await InitializeAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken) + { + if (CanExecute(isRestarting)) + { + await TerminateAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Determines whether the component can execute. + /// + /// If set to true indicates Umbraco is restarting. + /// + /// true if the component can execute; otherwise, false. + /// + protected virtual bool CanExecute(bool isRestarting) + => isRestarting is false; + + /// + /// Initializes the component. + /// + /// The cancellation token. Cancellation indicates that the start process has been aborted. + /// + /// A representing the asynchronous operation. + /// + protected abstract Task InitializeAsync(CancellationToken cancellationToken); + + /// + /// Terminates the component. + /// + /// The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful. + /// + /// A representing the asynchronous operation. + /// + protected virtual Task TerminateAsync(CancellationToken cancellationToken) + => Task.CompletedTask; +} diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index 506eb23134..ba49a6f482 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -5,59 +5,60 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Composing; /// -/// Represents the collection of implementations. +/// Represents the collection of implementations. /// -public class ComponentCollection : BuilderCollectionBase +public class ComponentCollection : BuilderCollectionBase { private const int LogThresholdMilliseconds = 100; - private readonly ILogger _logger; private readonly IProfilingLogger _profilingLogger; + private readonly ILogger _logger; - public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) : base(items) { _profilingLogger = profilingLogger; _logger = logger; } - public void Initialize() + public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken) { - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : _profilingLogger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) { - foreach (IComponent component in this) + foreach (IAsyncComponent component in this) { Type componentType = component.GetType(); - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Initializing {componentType.FullName}.", - $"Initialized {componentType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null : + _profilingLogger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { - component.Initialize(); + await component.InitializeAsync(isRestarting, cancellationToken); } } } } - public void Terminate() + public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken) { - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) + using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) + ? null + : _profilingLogger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) { // terminate components in reverse order - foreach (IComponent component in this.Reverse()) + foreach (IAsyncComponent component in this.Reverse()) { Type componentType = component.GetType(); - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Terminating {componentType.FullName}.", - $"Terminated {componentType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : _profilingLogger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { try { - component.Terminate(); - component.DisposeIfDisposable(); + await component.TerminateAsync(isRestarting, cancellationToken); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs index 04461db4fb..f4cdb80238 100644 --- a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs @@ -4,35 +4,33 @@ using Umbraco.Cms.Core.Logging; namespace Umbraco.Cms.Core.Composing; /// -/// Builds a . +/// Builds a . /// -public class - ComponentCollectionBuilder : OrderedCollectionBuilderBase +public class ComponentCollectionBuilder : OrderedCollectionBuilderBase { private const int LogThresholdMilliseconds = 100; protected override ComponentCollectionBuilder This => this; - protected override IEnumerable CreateItems(IServiceProvider factory) + protected override IEnumerable CreateItems(IServiceProvider factory) { IProfilingLogger logger = factory.GetRequiredService(); - using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration( - $"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) + using (logger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) { return base.CreateItems(factory); } } - protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + protected override IAsyncComponent CreateItem(IServiceProvider factory, Type itemType) { IProfilingLogger logger = factory.GetRequiredService(); - using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration( - $"Creating {itemType.FullName}.", - $"Created {itemType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + using (logger.IsEnabled(Logging.LogLevel.Debug) is false + ? null : + logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { return base.CreateItem(factory, itemType); } diff --git a/src/Umbraco.Core/Composing/ComponentComposer.cs b/src/Umbraco.Core/Composing/ComponentComposer.cs index 2a9641e64b..6beee43766 100644 --- a/src/Umbraco.Core/Composing/ComponentComposer.cs +++ b/src/Umbraco.Core/Composing/ComponentComposer.cs @@ -1,18 +1,23 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Core.Composing; /// -/// Provides a base class for composers which compose a component. +/// Provides a composer that appends a component. /// -/// The type of the component +/// The type of the component. +/// +/// Thanks to this class, a component that does not compose anything can be registered with one line: +/// +/// { } +/// ]]> +/// +/// public abstract class ComponentComposer : IComposer - where TComponent : IComponent + where TComponent : IAsyncComponent { /// - public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append(); - - // note: thanks to this class, a component that does not compose anything can be - // registered with one line: - // public class MyComponentComposer : ComponentComposer { } + public virtual void Compose(IUmbracoBuilder builder) + => builder.Components().Append(); } diff --git a/src/Umbraco.Core/Composing/IAsyncComponent.cs b/src/Umbraco.Core/Composing/IAsyncComponent.cs new file mode 100644 index 0000000000..b84e958879 --- /dev/null +++ b/src/Umbraco.Core/Composing/IAsyncComponent.cs @@ -0,0 +1,35 @@ +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a component. +/// +/// +/// +/// Components are created by DI and therefore must have a public constructor. +/// +/// +/// All components are terminated in reverse order when Umbraco terminates, and disposable components are disposed. +/// +/// +public interface IAsyncComponent +{ + /// + /// Initializes the component. + /// + /// If set to true indicates Umbraco is restarting. + /// The cancellation token. Cancellation indicates that the start process has been aborted. + /// + /// A representing the asynchronous operation. + /// + Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken); + + /// + /// Terminates the component. + /// + /// If set to true indicates Umbraco is restarting. + /// The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful. + /// + /// A representing the asynchronous operation. + /// + Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken); +} diff --git a/src/Umbraco.Core/Composing/IComponent.cs b/src/Umbraco.Core/Composing/IComponent.cs index d5655f8a1f..2c3b9b7e4f 100644 --- a/src/Umbraco.Core/Composing/IComponent.cs +++ b/src/Umbraco.Core/Composing/IComponent.cs @@ -1,28 +1,32 @@ namespace Umbraco.Cms.Core.Composing; -/// -/// Represents a component. -/// -/// -/// Components are created by DI and therefore must have a public constructor. -/// -/// All components are terminated in reverse order when Umbraco terminates, and -/// disposable components are disposed. -/// -/// -/// The Dispose method may be invoked more than once, and components -/// should ensure they support this. -/// -/// -public interface IComponent +/// +[Obsolete("Use IAsyncComponent instead. This interface will be removed in a future version.")] +public interface IComponent : IAsyncComponent { /// - /// Initializes the component. + /// Initializes the component. /// void Initialize(); /// - /// Terminates the component. + /// Terminates the component. /// void Terminate(); + + /// + Task IAsyncComponent.InitializeAsync(bool isRestarting, CancellationToken cancellationToken) + { + Initialize(); + + return Task.CompletedTask; + } + + /// + Task IAsyncComponent.TerminateAsync(bool isRestarting, CancellationToken cancellationToken) + { + Terminate(); + + return Task.CompletedTask; + } } diff --git a/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs b/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs new file mode 100644 index 0000000000..d4e326e058 --- /dev/null +++ b/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.Composing; + +/// +/// +/// By default, the component will not execute if Umbraco is restarting or the runtime level is not . +/// +public abstract class RuntimeAsyncComponentBase : AsyncComponentBase +{ + private readonly IRuntimeState _runtimeState; + + /// + /// Initializes a new instance of the class. + /// + /// State of the Umbraco runtime. + protected RuntimeAsyncComponentBase(IRuntimeState runtimeState) + => _runtimeState = runtimeState; + + /// + protected override bool CanExecute(bool isRestarting) + => base.CanExecute(isRestarting) && _runtimeState.Level == RuntimeLevel.Run; +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs index af42e09936..e1536f9a4d 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -21,7 +21,7 @@ public static partial class UmbracoBuilderExtensions /// The builder. /// public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) - where T : IComponent + where T : IAsyncComponent { builder.Components().Append(); diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 2ef1f9194f..21fa8fa3b4 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -41,8 +41,9 @@ public static class ObjectExtensions public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); /// + /// Disposes the object if it implements . /// - /// + /// The object. public static void DisposeIfDisposable(this object input) { if (input is IDisposable disposable) diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index 9172359eb0..63c733f3cb 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,28 +1,26 @@ namespace Umbraco.Cms.Core.Notifications; /// -/// Notification that occurs at the very end of the Umbraco boot process (after all s are -/// initialized). +/// Notification that occurs at the very end of the Umbraco boot process (after all components are initialized). +/// +public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification +{ + /// + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) - { - RuntimeLevel = runtimeLevel; - IsRestarting = isRestarting; - } + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } /// - /// Gets the runtime level. + /// Gets the runtime level. /// /// - /// The runtime level. + /// The runtime level. /// public RuntimeLevel RuntimeLevel { get; } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index d33233d438..43058fe27f 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,17 +1,16 @@ namespace Umbraco.Cms.Core.Notifications; - +/// +/// Notification that occurs when Umbraco is shutting down (after all components are terminated). +/// +public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification +{ /// - /// Notification that occurs when Umbraco is shutting down (after all s are terminated). + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) + => IsRestarting = isRestarting; /// public bool IsRestarting { get; } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 42f7f6de3f..2e46049042 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -222,7 +222,7 @@ public class CoreRuntime : IRuntime } // Initialize the components - _components.Initialize(); + await _components.InitializeAsync(isRestarting, cancellationToken); await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); @@ -236,7 +236,7 @@ public class CoreRuntime : IRuntime private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { - _components.Terminate(); + await _components.TerminateAsync(isRestarting, cancellationToken); await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index ca47bfbd97..94bff4a0c9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -80,7 +80,9 @@ public class ComponentTests Options.Create(new ContentSettings())); var eventAggregator = Mock.Of(); var scopeProvider = new ScopeProvider( - new AmbientScopeStack(), new AmbientScopeContextStack(),Mock.Of(), + new AmbientScopeStack(), + new AmbientScopeContextStack(), + Mock.Of(), f, fs, new TestOptionsMonitor(coreDebug), @@ -113,7 +115,7 @@ public class ComponentTests Mock.Of()); [Test] - public void Boot1A() + public async Task Boot1A() { var register = MockRegister(); var composition = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); @@ -157,7 +159,7 @@ public class ComponentTests { return Mock.Of>(); } - + if (type == typeof(ILogger)) { return Mock.Of>(); @@ -176,9 +178,9 @@ public class ComponentTests var components = builder.CreateCollection(factory); Assert.IsEmpty(components); - components.Initialize(); + await components.InitializeAsync(false, default); Assert.IsEmpty(Initialized); - components.Terminate(); + await components.TerminateAsync(false, default); Assert.IsEmpty(Terminated); } @@ -277,7 +279,7 @@ public class ComponentTests } [Test] - public void Initialize() + public async Task Initialize() { Composed.Clear(); Initialized.Clear(); @@ -324,7 +326,7 @@ public class ComponentTests { return Mock.Of>(); } - + if (type == typeof(IServiceProviderIsService)) { return Mock.Of(); @@ -347,11 +349,11 @@ public class ComponentTests var components = builder.CreateCollection(factory); Assert.IsEmpty(Initialized); - components.Initialize(); + await components.InitializeAsync(false, default); AssertTypeArray(TypeArray(), Initialized); Assert.IsEmpty(Terminated); - components.Terminate(); + await components.TerminateAsync(false, default); AssertTypeArray(TypeArray(), Terminated); } From 142db8c0fb543c154db30a5fe95b1f4abfd5a2fe Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:10:46 +0200 Subject: [PATCH 23/38] V15 QA Enabled Nightly E2E Pipeline to run on V15 (#17103) * Uncommented * Added timeout --- build/nightly-E2E-test-pipelines.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml index b1dfe9261c..ce67b778e3 100644 --- a/build/nightly-E2E-test-pipelines.yml +++ b/build/nightly-E2E-test-pipelines.yml @@ -9,8 +9,7 @@ schedules: branches: include: - v14/dev - ## Uncomment after merged to v15/dev - ## - v15/dev + - v15/dev variables: nodeVersion: 20 @@ -109,7 +108,7 @@ stages: # E2E Tests - job: displayName: E2E Tests (SQLite) - timeoutInMinutes: 120 + timeoutInMinutes: 180 variables: # Connection string CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=Umbraco;Mode=Memory;Cache=Shared;Foreign Keys=True;Pooling=True @@ -244,7 +243,7 @@ stages: - job: displayName: E2E Tests (SQL Server) condition: and(succeeded(), ${{ eq(parameters.runSqlServerE2ETests, true) }}) - timeoutInMinutes: 120 + timeoutInMinutes: 180 variables: # Connection string CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True From d2504e3688c720f5365ec3db7c39ad1498851ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 23 Sep 2024 14:49:07 +0200 Subject: [PATCH 24/38] update uui-css dependency --- src/Umbraco.Web.UI.Login/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index 7b0e3455c4..a55731fb70 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@umbraco-cms/backoffice": "^14.0.0", - "@umbraco-ui/uui-css": "^1.8.0", + "@umbraco-ui/uui-css": "^1.10.0", "msw": "^2.3.0", "typescript": "^5.4.5", "vite": "^5.2.11", From e342e795dd92262032970839e57d1af0339463cd Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 24 Sep 2024 09:39:23 +0200 Subject: [PATCH 25/38] V15: Cache Seeding (#17102) * Update to dotnet 9 and update nuget packages * Update umbraco code version * Update Directory.Build.props Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> * Include preview version in pipeline * update template projects * update global json with specific version * Update version.json to v15 * Rename TrimStart and TrimEnd to string specific * Rename to Exact * Update global.json Co-authored-by: Ronald Barendse * Remove includePreviewVersion * Rename to trim exact * Add new Hybridcache project * Add tests * Start implementing PublishedContent.cs * Implement repository for content * Refactor to use async everywhere * Add cache refresher * make public as needed for serialization * Use content type cache to get content type out * Refactor to use ContentCacheNode model, that goes in the memory cache * Remove content node kit as its not needed * Implement tests for ensuring caching * Implement better asserts * Implement published property * Refactor to use mapping * Rename to document tests * Update to test properties * Create more tests * Refactor mock tests into own file * Update property test * Fix published version of content * Change default cache level to elements * Refactor to always have draft * Refactor to not use PublishedModelFactory * Added tests * Added and updated tests * Fixed tests * Don't return empty object with id * More tests * Added key * Another key * Refactor CacheService to be responsible for using the hybrid cache * Use notification handler to remove deleted content from cache * Add more tests for missing functions * Implement missing methods * Remove HasContent as it pertains to routing * Fik up test * formatting * refactor variable names * Implement variant tests * Map all the published content properties * Get item out of cache first, to assert updated * Implement member cache * Add member test * Implement media cache * Implement property tests for media tests * Refactor tests to use extension method * Add more media tests * Refactor properties to no longer have element caching * Don't use property cache level * Start implementing seeding * Only seed when main * Add Immutable for performance * Implement permanent seeding of content * Implement cache settings * Implement tests for seeding * Update package version * start refactoring nurepo * Refactor so draft & published nodes are cached individually * Refactor RefreshContent to take node instead of IContent * Refactor media to also use cache nodes * Remove member from repo as it isn't cached * Refactor media to not include preview, as media has no draft * create new benchmark project * POC Integration benchmarks with custom api controllers * Start implementing content picker tests * Implement domain cache * Rework content cache to implement interface * Start implementing elements cache * Implement published snapshot service * Publish snapshot tests * Use snapshot for elements cache * Create test proving we don't clear cache when updating content picker * Clear entire elements cache * Remove properties from element cache, when content gets updated. * Rename methods to async * Refactor to use old cache interfaces instead of new ones * Remove snapshot, as it is no longer needed * Fix tests building * Refactor domaincache to not have snapshots * Delete benchmarks * Delete benchmarks * Add HybridCacheProject to Umbraco * Add comment to route value transformer * Implement is draft * remove snapshot from property * V15 updated the hybrid caching integration tests to use ContentEditingService (#16947) * Added builder extension withParentKey * Created builder with ContentEditingService * Added usage of the ContentEditingService to SETUP * Started using ContentEditingService builder in tests * Updated builder extensions * Fixed builder * Clean up * Clean up, not done * Added Ids * Remove entries from cache on delete * Fix up seeding logic * Don't register hybrid cache twice * Change seeded entry options * Update hybrid cache package * Fix up published property to work with delivery api again * Fix dependency injection to work with tests * Fix naming * Dont make caches nullable * Make content node sealed * Remove path and other unused from content node * Remove hacky 2 phase ctor * Refactor to actually set content templates * Remove umbraco context * Remove "HasBy" methods * rename property data * Delete obsolete legacy stuff * Add todo for making expiration configurable * Add todo in UmbracoContext * Add clarifying comment in content factory * Remove xml stuff from published property * Fix according to review * Make content type cache injectible * Make content type cache injectible * Rename to database cache repository * Rename to document cache * Add TODO * Refactor to async * Rename to async * Make everything async * Remove duplicate line from json schema * Move Hybrid cache project * Remove leftover file * Refactor to use keys * Refactor published content to no longer have content data, as it is on the node itself * Refactor to member to use proper content node ctor * Move tests to own folder * Add immutable objects to property and content data for performance * Make property data public * Fix member caching to be singleton * Obsolete GetContentType * Remove todo * Fix naming * Fix lots of exposed errors due to scope test * Add final scope tests * Rename to document cache service * Rename test files * Create new doc type tests * Add ignore to tests * Start implementing refresh for content type save * Clear contenttype cache when contenttype is updated * Fix test Teh contenttype is not upated unless the property is dirty * Updated tests * Added tests * Use init for ContentSourceDto * Startup of setup * Fix get by key in PublishedContentTypeCache * Remove ContentType from PublishedContentTypeCache when contenttype is deleted * Created interfaces for the builder with the necessary properties * Created builder for PropertyTypeContainer * Created builder for PropertyTypeEditing * Created builder for PropertyTypeValidationEditing * Made adjustments to the builder * Updated name of usage * Commented out to test * Cleaned up builders * Updated integration test setup * Moved tests * Added interface * Add IDocumentSeedKeyProvider and migrate existing logic to seed key provider * Added functionality to the INavigationQueryService to get root keys * Fixed issue with navigation * Created helper to Convert a IContentType to ContentTypeUpdateModel * Added interfaces * Added builder * Cleaned up builders and added fixes * Added tests for PublishedContentTypeCache * Applied changes in builder * Add BreadthFirstKeyProvider * Use ISet for seedkey providers * Implement GetContentSource by key * Seed the cache with keys provided by seed key providers * Builder updates * Test setup updates * Updated tests * Dont require contenttype keys for seeding * Fix cache settings * Don't inject cache settings into SeedingNotificationHandler * Fix tests * Use enlistment for setting updated cache item * Pin seeded nodes for longer * Fix BreadthFirstKeyProvider * Fix ContentTypeSeedKeyProvider * Fix tests * Only seed published documents * Only cache published if contentCacheNode is not draft * Fix incorrect templateId * Removed unnecessary setup * initialized value * Fixed template test * Removed test * Updated tests * Removed code that was not used * Removed unused cacheSettings * Re-organize to support media cache seeding * Add MediaBreadthFirstKeyProvider * Seed media * Don't use IdKeyMap when removing content from cache * Don't clear IdKeyMap in DocumentCacheService * Add unit tests * Don't use IdKeyMap when deleting media * Add default value to timespan * Use cancellation tokens when doing loop * Fixed Models Builder error --------- Co-authored-by: Zeegaan Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Co-authored-by: Ronald Barendse Co-authored-by: Andreas Zerbst Co-authored-by: Sven Geusens Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Bjarke Berg --- src/Umbraco.Core/Constants-SqlTemplates.cs | 1 + .../Factories/NavigationFactory.cs | 7 +- src/Umbraco.Core/Models/CacheSettings.cs | 18 +- .../ContentNavigationServiceBase.cs | 13 +- .../Navigation/INavigationQueryService.cs | 1 + .../UmbracoBuilderExtensions.cs | 13 + .../Factories/CacheNodeFactory.cs | 2 +- .../IDocumentSeedKeyProvider.cs | 6 + .../IMediaSeedKeyProvider.cs | 6 + .../ISeedKeyProvider.cs | 10 + .../CacheRefreshingNotificationHandler.cs | 4 +- .../SeedingNotificationHandler.cs | 27 +- .../Persistence/DatabaseCacheRepository.cs | 83 +++++- .../Persistence/IDatabaseCacheRepository.cs | 12 + .../BreadthFirstKeyProvider.cs | 67 +++++ .../Document/ContentTypeSeedKeyProvider.cs | 32 +++ .../DocumentBreadthFirstKeyProvider.cs | 15 ++ .../Media/MediaBreadthFirstKeyProvider.cs | 14 + .../Services/DocumentCacheService.cs | 153 ++++++++--- .../Services/IDocumentCacheService.cs | 4 +- .../Services/IMediaCacheService.cs | 4 +- .../Services/MediaCacheService.cs | 75 +++++- .../Umbraco.PublishedCache.HybridCache.csproj | 3 + .../Builders/ContentEditingBuilder.cs | 53 ++-- .../Builders/ContentTypeEditingBuilder.cs | 240 ++++++++++++++++++ .../Builders/ContentTypeSortBuilder.cs | 5 + .../Builders/Extensions/BuilderExtensions.cs | 7 + .../Interfaces/IWIthContainerKeyBuilder.cs | 6 + .../Interfaces/IWithDataTypeKeyBuilder.cs | 6 + .../Builders/Interfaces/IWithLabelOnTop.cs | 6 + .../Interfaces/IWithMandatoryBuilder.cs | 6 + .../IWithMandatoryMessageBuilder.cs | 6 + .../IWithRegularExpressionBuilder.cs | 6 + .../IWithRegularExpressionMessage.cs | 6 + .../Builders/Interfaces/IWithTypeBuilder.cs | 6 + .../Interfaces/IWithVariesByCultureBuilder.cs | 6 + .../Interfaces/IWithVariesBySegmentBuilder.cs | 6 + .../Builders/PropertyTypeAppearanceBuilder.cs | 22 ++ .../Builders/PropertyTypeContainerBuilder.cs | 66 +++++ .../Builders/PropertyTypeEditingBuilder.cs | 176 +++++++++++++ .../PropertyTypeValidationEditingBuilder.cs | 55 ++++ .../TestHelpers/ContentTypeUpdateHelper.cs | 83 ++++++ ...mbracoIntegrationTestWithContentEditing.cs | 95 ++++--- .../Cache/PublishedContentTypeCacheTests.cs | 63 +++++ .../DocumentHybridCacheDocumentTypeTests.cs | 34 +-- .../DocumentHybridCacheMockTests.cs | 115 ++++++--- .../DocumentHybridCachePropertyTest.cs | 65 +++-- .../DocumentHybridCacheTemplateTests.cs | 44 ++++ .../DocumentHybridCacheTests.cs | 197 +++++++------- .../DocumentHybridCacheVariantsTests.cs | 115 ++++----- .../DocumentBreadthFirstKeyProviderTests.cs | 117 +++++++++ 51 files changed, 1769 insertions(+), 413 deletions(-) create mode 100644 src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs create mode 100644 src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs create mode 100644 tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs index ad5b326035..3641510fd6 100644 --- a/src/Umbraco.Core/Constants-SqlTemplates.cs +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -30,6 +30,7 @@ public static partial class Constants public static class NuCacheDatabaseDataSource { public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId"; + public const string WhereNodeKey = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeKey"; public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX"; public const string SourcesSelectUmbracoNodeJoin = diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs index 316c6031d6..815312e048 100644 --- a/src/Umbraco.Core/Factories/NavigationFactory.cs +++ b/src/Umbraco.Core/Factories/NavigationFactory.cs @@ -9,11 +9,10 @@ internal static class NavigationFactory /// /// Builds a dictionary of NavigationNode objects from a given dataset. /// + /// A dictionary of objects with key corresponding to their unique Guid. /// The objects used to build the navigation nodes dictionary. - /// A dictionary of objects with key corresponding to their unique Guid. - public static ConcurrentDictionary BuildNavigationDictionary(IEnumerable entities) + public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure,IEnumerable entities) { - var nodesStructure = new ConcurrentDictionary(); var entityList = entities.ToList(); var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); @@ -39,7 +38,5 @@ internal static class NavigationFactory parentNode.AddChild(node); } } - - return nodesStructure; } } diff --git a/src/Umbraco.Core/Models/CacheSettings.cs b/src/Umbraco.Core/Models/CacheSettings.cs index dcd7211347..2d4373a4da 100644 --- a/src/Umbraco.Core/Models/CacheSettings.cs +++ b/src/Umbraco.Core/Models/CacheSettings.cs @@ -1,13 +1,29 @@ -using Umbraco.Cms.Core.Configuration.Models; +using System.ComponentModel; +using Umbraco.Cms.Core.Configuration.Models; namespace Umbraco.Cms.Core.Models; [UmbracoOptions(Constants.Configuration.ConfigCache)] public class CacheSettings { + internal const int StaticDocumentBreadthFirstSeedCount = 100; + + internal const int StaticMediaBreadthFirstSeedCount = 100; + internal const string StaticSeedCacheDuration = "365.00:00:00"; + /// /// Gets or sets a value for the collection of content type ids to always have in the cache. /// public List ContentTypeKeys { get; set; } = new(); + + [DefaultValue(StaticDocumentBreadthFirstSeedCount)] + public int DocumentBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount; + + + [DefaultValue(StaticMediaBreadthFirstSeedCount)] + public int MediaBreadthFirstSeedCount { get; set; } = StaticDocumentBreadthFirstSeedCount; + + [DefaultValue(StaticSeedCacheDuration)] + public TimeSpan SeedCacheDuration { get; set; } = TimeSpan.Parse(StaticSeedCacheDuration); } diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index e5755c8d87..394223c311 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -36,6 +36,9 @@ internal abstract class ContentNavigationServiceBase public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); + public bool TryGetRootKeys(out IEnumerable childrenKeys) + => TryGetRootKeysFromStructure(_navigationStructure, out childrenKeys); + public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); @@ -162,6 +165,7 @@ internal abstract class ContentNavigationServiceBase _recycleBinNavigationStructure.TryRemove(key, out _); } + /// /// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed. /// Only relevant for items in the content and media trees (which have readLock values of -333 or -334). @@ -184,7 +188,7 @@ internal abstract class ContentNavigationServiceBase _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) : _navigationRepository.GetContentNodesByObjectType(objectTypeKey); - _navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels); + NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels); } private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey) @@ -213,6 +217,13 @@ internal abstract class ContentNavigationServiceBase return true; } + private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable childrenKeys) + { + // TODO can we make this more efficient? + childrenKeys = structure.Values.Where(x=>x.Parent is null).Select(x=>x.Key); + return true; + } + private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) { var descendants = new List(); diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 4e28f80bb6..9b6fb9807d 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -9,6 +9,7 @@ public interface INavigationQueryService bool TryGetParentKey(Guid childKey, out Guid? parentKey); bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); + bool TryGetRootKeys(out IEnumerable childrenKeys); bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs index 6ad695c154..984fcbe110 100644 --- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs @@ -11,6 +11,8 @@ using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media; using Umbraco.Cms.Infrastructure.HybridCache.Serialization; using Umbraco.Cms.Infrastructure.HybridCache.Services; @@ -62,6 +64,17 @@ public static class UmbracoBuilderExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + builder.AddCacheSeeding(); + return builder; + } + + private static IUmbracoBuilder AddCacheSeeding(this IUmbracoBuilder builder) + { + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + + + builder.Services.AddSingleton(); return builder; } } diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs index 7fd91c4603..accc962c5c 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/CacheNodeFactory.cs @@ -17,7 +17,7 @@ internal class CacheNodeFactory : ICacheNodeFactory public ContentCacheNode ToContentCacheNode(IContent content, bool preview) { - ContentData contentData = GetContentData(content, !preview, preview ? content.PublishTemplateId : content.TemplateId); + ContentData contentData = GetContentData(content, !preview, preview ? content.TemplateId : content.PublishTemplateId); return new ContentCacheNode { Id = content.Id, diff --git a/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs new file mode 100644 index 0000000000..9fa39fd072 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/IDocumentSeedKeyProvider.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface IDocumentSeedKeyProvider : ISeedKeyProvider +{ + +} diff --git a/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs new file mode 100644 index 0000000000..54ec4926fd --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/IMediaSeedKeyProvider.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface IMediaSeedKeyProvider : ISeedKeyProvider +{ + +} diff --git a/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs new file mode 100644 index 0000000000..5883c89dc3 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/ISeedKeyProvider.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Infrastructure.HybridCache; + +public interface ISeedKeyProvider +{ + /// + /// Gets keys of documents that should be seeded into the cache. + /// + /// Keys to seed + ISet GetSeedKeys(); +} diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs index 105fad1d9d..a38c0408a1 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/CacheRefreshingNotificationHandler.cs @@ -51,7 +51,7 @@ internal sealed class CacheRefreshingNotificationHandler : foreach (IContent deletedEntity in notification.DeletedEntities) { await RefreshElementsCacheAsync(deletedEntity); - await _documentCacheService.DeleteItemAsync(deletedEntity.Id); + await _documentCacheService.DeleteItemAsync(deletedEntity); } } @@ -66,7 +66,7 @@ internal sealed class CacheRefreshingNotificationHandler : foreach (IMedia deletedEntity in notification.DeletedEntities) { await RefreshElementsCacheAsync(deletedEntity); - await _mediaCacheService.DeleteItemAsync(deletedEntity.Id); + await _mediaCacheService.DeleteItemAsync(deletedEntity); } } diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs index d0dfa76b67..1cea1a2360 100644 --- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs +++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs @@ -1,7 +1,7 @@ -using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HybridCache.Services; namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; @@ -9,13 +9,28 @@ namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers; internal class SeedingNotificationHandler : INotificationAsyncHandler { private readonly IDocumentCacheService _documentCacheService; - private readonly CacheSettings _cacheSettings; + private readonly IMediaCacheService _mediaCacheService; + private readonly IRuntimeState _runtimeState; - public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IOptions cacheSettings) + public SeedingNotificationHandler(IDocumentCacheService documentCacheService, IMediaCacheService mediaCacheService, IRuntimeState runtimeState) { _documentCacheService = documentCacheService; - _cacheSettings = cacheSettings.Value; + _mediaCacheService = mediaCacheService; + _runtimeState = runtimeState; } - public async Task HandleAsync(UmbracoApplicationStartedNotification notification, CancellationToken cancellationToken) => await _documentCacheService.SeedAsync(_cacheSettings.ContentTypeKeys); + public async Task HandleAsync(UmbracoApplicationStartedNotification notification, + CancellationToken cancellationToken) + { + + if (_runtimeState.Level <= RuntimeLevel.Install) + { + return; + } + + await Task.WhenAll( + _documentCacheService.SeedAsync(cancellationToken), + _mediaCacheService.SeedAsync(cancellationToken) + ); + } } diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs index d49d2f8799..0f917508aa 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs @@ -65,8 +65,13 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe { IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - // always refresh the edited data - await OnRepositoryRefreshed(serializer, contentCacheNode, true); + // We always cache draft and published separately, so we only want to cache drafts if the node is a draft type. + if (contentCacheNode.IsDraft) + { + await OnRepositoryRefreshed(serializer, contentCacheNode, true); + // if it's a draft node we don't need to worry about the published state + return; + } switch (publishedState) { @@ -208,11 +213,36 @@ AND cmsContentNu.nodeId IS NULL return CreateContentNodeKit(dto, serializer, preview); } - public IEnumerable GetContentByContentTypeKey(IEnumerable keys) + public async Task GetContentSourceAsync(Guid key, bool preview = false) { + Sql? sql = SqlContentSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Document)) + .Append(SqlWhereNodeKey(SqlContext, key)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto == null) + { + return null; + } + + if (preview is false && dto.PubDataRaw is null && dto.PubData is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); + return CreateContentNodeKit(dto, serializer, preview); + } + + private IEnumerable GetContentSourceByDocumentTypeKey(IEnumerable documentTypeKeys) + { + Guid[] keys = documentTypeKeys.ToArray(); if (keys.Any() is false) { - yield break; + return []; } Sql? sql = SqlContentSourcesSelect() @@ -222,17 +252,26 @@ AND cmsContentNu.nodeId IS NULL .WhereIn(x => x.UniqueId, keys,"n") .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + return GetContentNodeDtos(sql); + } + + public IEnumerable GetContentByContentTypeKey(IEnumerable keys) + { + IEnumerable dtos = GetContentSourceByDocumentTypeKey(keys); + IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document); - IEnumerable dtos = GetContentNodeDtos(sql); - foreach (ContentSourceDto row in dtos) { yield return CreateContentNodeKit(row, serializer, row.Published is false); } } + /// + public IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false) + => GetContentSourceByDocumentTypeKey(keys).Where(x => x.Published == published).Select(x => x.Key); + public async Task GetMediaSourceAsync(int id) { Sql? sql = SqlMediaSourcesSelect() @@ -252,6 +291,25 @@ AND cmsContentNu.nodeId IS NULL return CreateMediaNodeKit(dto, serializer); } + public async Task GetMediaSourceAsync(Guid key) + { + Sql? sql = SqlMediaSourcesSelect() + .Append(SqlObjectTypeNotTrashed(SqlContext, Constants.ObjectTypes.Media)) + .Append(SqlWhereNodeKey(SqlContext, key)) + .Append(SqlOrderByLevelIdSortOrder(SqlContext)); + + ContentSourceDto? dto = await Database.FirstOrDefaultAsync(sql); + + if (dto is null) + { + return null; + } + + IContentCacheDataSerializer serializer = + _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media); + return CreateMediaNodeKit(dto, serializer); + } + private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview) { // use a custom SQL to update row version on each update @@ -642,6 +700,19 @@ WHERE cmsContentNu.nodeId IN ( return sql; } + private Sql SqlWhereNodeKey(ISqlContext sqlContext, Guid key) + { + ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; + + SqlTemplate sqlTemplate = sqlContext.Templates.Get( + Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeKey, + builder => + builder.Where(x => x.UniqueId == SqlTemplate.Arg("key"))); + + Sql sql = sqlTemplate.Sql(key); + return sql; + } + private Sql SqlOrderByLevelIdSortOrder(ISqlContext sqlContext) { ISqlSyntaxProvider syntax = sqlContext.SqlSyntax; diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs index 47c18c07e1..6a88d5405e 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/IDatabaseCacheRepository.cs @@ -8,10 +8,22 @@ internal interface IDatabaseCacheRepository Task GetContentSourceAsync(int id, bool preview = false); + Task GetContentSourceAsync(Guid key, bool preview = false); + Task GetMediaSourceAsync(int id); + Task GetMediaSourceAsync(Guid key); + + IEnumerable GetContentByContentTypeKey(IEnumerable keys); + /// + /// Gets all content keys of specific document types + /// + /// The document types to find content using. + /// The keys of all content use specific document types. + IEnumerable GetContentKeysByContentTypeKeys(IEnumerable keys, bool published = false); + /// /// Refreshes the nucache database row for the given cache node /> /// diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..99a5fe50a3 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/BreadthFirstKeyProvider.cs @@ -0,0 +1,67 @@ +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders; + +public abstract class BreadthFirstKeyProvider +{ + private readonly INavigationQueryService _navigationQueryService; + private readonly int _seedCount; + + public BreadthFirstKeyProvider(INavigationQueryService navigationQueryService, int seedCount) + { + _navigationQueryService = navigationQueryService; + _seedCount = seedCount; + } + + public ISet GetSeedKeys() + { + if (_seedCount == 0) + { + return new HashSet(); + } + + Queue keyQueue = new(); + HashSet keys = []; + int keyCount = 0; + + if (_navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys) is false) + { + return new HashSet(); + } + + foreach (Guid key in rootKeys) + { + keyCount++; + keys.Add(key); + keyQueue.Enqueue(key); + if (keyCount == _seedCount) + { + return keys; + } + } + + while (keyQueue.Count > 0 && keyCount < _seedCount) + { + Guid key = keyQueue.Dequeue(); + + if (_navigationQueryService.TryGetChildrenKeys(key, out IEnumerable childKeys) is false) + { + continue; + } + + foreach (Guid childKey in childKeys) + { + keys.Add(childKey); + keyCount++; + if (keyCount == _seedCount) + { + return keys; + } + + keyQueue.Enqueue(childKey); + } + } + + return keys; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs new file mode 100644 index 0000000000..bb0d721d63 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/ContentTypeSeedKeyProvider.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.HybridCache.Persistence; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +internal sealed class ContentTypeSeedKeyProvider : IDocumentSeedKeyProvider +{ + private readonly ICoreScopeProvider _scopeProvider; + private readonly IDatabaseCacheRepository _databaseCacheRepository; + private readonly CacheSettings _cacheSettings; + + public ContentTypeSeedKeyProvider( + ICoreScopeProvider scopeProvider, + IDatabaseCacheRepository databaseCacheRepository, + IOptions cacheSettings) + { + _scopeProvider = scopeProvider; + _databaseCacheRepository = databaseCacheRepository; + _cacheSettings = cacheSettings.Value; + } + + public ISet GetSeedKeys() + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + var documentKeys = _databaseCacheRepository.GetContentKeysByContentTypeKeys(_cacheSettings.ContentTypeKeys, published: true).ToHashSet(); + scope.Complete(); + + return documentKeys; + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..1e991d6277 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Document/DocumentBreadthFirstKeyProvider.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +internal sealed class DocumentBreadthFirstKeyProvider : BreadthFirstKeyProvider, IDocumentSeedKeyProvider +{ + public DocumentBreadthFirstKeyProvider( + IDocumentNavigationQueryService documentNavigationQueryService, + IOptions cacheSettings) + : base(documentNavigationQueryService, cacheSettings.Value.DocumentBreadthFirstSeedCount) + { + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs new file mode 100644 index 0000000000..657ec4b8a0 --- /dev/null +++ b/src/Umbraco.PublishedCache.HybridCache/SeedKeyProviders/Media/MediaBreadthFirstKeyProvider.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; + +namespace Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Media; + +internal sealed class MediaBreadthFirstKeyProvider : BreadthFirstKeyProvider, IMediaSeedKeyProvider +{ + public MediaBreadthFirstKeyProvider( + IMediaNavigationQueryService navigationQueryService, IOptions cacheSettings) + : base(navigationQueryService, cacheSettings.Value.MediaBreadthFirstSeedCount) + { + } +} diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index b0aa936793..b91ea182f2 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -1,4 +1,7 @@ -using Microsoft.Extensions.Caching.Hybrid; +using System.Diagnostics; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -6,6 +9,7 @@ using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.HybridCache.Services; @@ -17,7 +21,30 @@ internal sealed class DocumentCacheService : IDocumentCacheService private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; private readonly IPublishedContentFactory _publishedContentFactory; private readonly ICacheNodeFactory _cacheNodeFactory; + private readonly IEnumerable _seedKeyProviders; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly CacheSettings _cacheSettings; + private HashSet? _seedKeys; + private HashSet SeedKeys + { + get + { + if (_seedKeys is not null) + { + return _seedKeys; + } + + _seedKeys = []; + + foreach (IDocumentSeedKeyProvider provider in _seedKeyProviders) + { + _seedKeys.UnionWith(provider.GetSeedKeys()); + } + + return _seedKeys; + } + } public DocumentCacheService( IDatabaseCacheRepository databaseCacheRepository, @@ -25,7 +52,10 @@ internal sealed class DocumentCacheService : IDocumentCacheService ICoreScopeProvider scopeProvider, Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, IPublishedContentFactory publishedContentFactory, - ICacheNodeFactory cacheNodeFactory) + ICacheNodeFactory cacheNodeFactory, + IEnumerable seedKeyProviders, + IOptions cacheSettings, + IPublishedModelFactory publishedModelFactory) { _databaseCacheRepository = databaseCacheRepository; _idKeyMap = idKeyMap; @@ -33,25 +63,21 @@ internal sealed class DocumentCacheService : IDocumentCacheService _hybridCache = hybridCache; _publishedContentFactory = publishedContentFactory; _cacheNodeFactory = cacheNodeFactory; + _seedKeyProviders = seedKeyProviders; + _publishedModelFactory = publishedModelFactory; + _cacheSettings = cacheSettings.Value; } - // TODO: Stop using IdKeyMap for these, but right now we both need key and id for caching.. public async Task GetByKeyAsync(Guid key, bool preview = false) { - Attempt idAttempt = _idKeyMap.GetIdForKey(key, UmbracoObjectTypes.Document); - if (idAttempt.Success is false) - { - return null; - } - using ICoreScope scope = _scopeProvider.CreateCoreScope(); ContentCacheNode? contentCacheNode = await _hybridCache.GetOrCreateAsync( GetCacheKey(key, preview), // Unique key to the cache entry - async cancel => await _databaseCacheRepository.GetContentSourceAsync(idAttempt.Result, preview)); + async cancel => await _databaseCacheRepository.GetContentSourceAsync(key, preview)); scope.Complete(); - return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory); } public async Task GetByIdAsync(int id, bool preview = false) @@ -67,37 +93,55 @@ internal sealed class DocumentCacheService : IDocumentCacheService GetCacheKey(keyAttempt.Result, preview), // Unique key to the cache entry async cancel => await _databaseCacheRepository.GetContentSourceAsync(id, preview)); scope.Complete(); - return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview); + return contentCacheNode is null ? null : _publishedContentFactory.ToIPublishedContent(contentCacheNode, preview).CreateModel(_publishedModelFactory);; } - public async Task SeedAsync(IReadOnlyCollection contentTypeKeys) + public async Task SeedAsync(CancellationToken cancellationToken) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - IEnumerable contentCacheNodes = _databaseCacheRepository.GetContentByContentTypeKey(contentTypeKeys); - foreach (ContentCacheNode contentCacheNode in contentCacheNodes) + + foreach (Guid key in SeedKeys) { - if (contentCacheNode.IsDraft) + if(cancellationToken.IsCancellationRequested) { - continue; + break; } - // TODO: Make these expiration dates configurable. - // Never expire seeded values, we cannot do TimeSpan.MaxValue sadly, so best we can do is a year. - var entryOptions = new HybridCacheEntryOptions - { - Expiration = TimeSpan.FromDays(365), - LocalCacheExpiration = TimeSpan.FromDays(365), - }; + var cacheKey = GetCacheKey(key, false); - await _hybridCache.SetAsync( - GetCacheKey(contentCacheNode.Key, false), - contentCacheNode, - entryOptions); + // We'll use GetOrCreateAsync because it may be in the second level cache, in which case we don't have to re-seed. + ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync( + cacheKey, + async cancel => + { + ContentCacheNode? cacheNode = await _databaseCacheRepository.GetContentSourceAsync(key, false); + + // We don't want to seed drafts + if (cacheNode is null || cacheNode.IsDraft) + { + return null; + } + + return cacheNode; + }, + GetSeedEntryOptions()); + + // If the value is null, it's likely because + if (cachedValue is null) + { + await _hybridCache.RemoveAsync(cacheKey); + } } scope.Complete(); } + private HybridCacheEntryOptions GetSeedEntryOptions() => new() + { + Expiration = _cacheSettings.SeedCacheDuration, + LocalCacheExpiration = _cacheSettings.SeedCacheDuration + }; + public async Task HasContentByIdAsync(int id, bool preview = false) { Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); @@ -122,34 +166,67 @@ internal sealed class DocumentCacheService : IDocumentCacheService { using ICoreScope scope = _scopeProvider.CreateCoreScope(); + bool isSeeded = SeedKeys.Contains(content.Key); + // Always set draft node // We have nodes seperate in the cache, cause 99% of the time, you are only using one // and thus we won't get too much data when retrieving from the cache. - var draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true); - await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)); + ContentCacheNode draftCacheNode = _cacheNodeFactory.ToContentCacheNode(content, true); + await _databaseCacheRepository.RefreshContentAsync(draftCacheNode, content.PublishedState); + _scopeProvider.Context?.Enlist($"UpdateMemoryCache_Draft_{content.Key}", completed => + { + if(completed is false) + { + return; + } + + RefreshHybridCache(draftCacheNode, GetCacheKey(content.Key, true), isSeeded).GetAwaiter().GetResult(); + }, 1); if (content.PublishedState == PublishedState.Publishing) { var publishedCacheNode = _cacheNodeFactory.ToContentCacheNode(content, false); - await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)); + await _databaseCacheRepository.RefreshContentAsync(publishedCacheNode, content.PublishedState); + _scopeProvider.Context?.Enlist($"UpdateMemoryCache_{content.Key}", completed => + { + if(completed is false) + { + return; + } + + RefreshHybridCache(publishedCacheNode, GetCacheKey(content.Key, false), isSeeded).GetAwaiter().GetResult(); + }, 1); } scope.Complete(); } + private async Task RefreshHybridCache(ContentCacheNode cacheNode, string cacheKey, bool isSeeded) + { + // If it's seeded we want it to stick around the cache for longer. + if (isSeeded) + { + await _hybridCache.SetAsync( + cacheKey, + cacheNode, + GetSeedEntryOptions()); + } + else + { + await _hybridCache.SetAsync(cacheKey, cacheNode); + } + } + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; - public async Task DeleteItemAsync(int id) + public async Task DeleteItemAsync(IContentBase content) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - await _databaseCacheRepository.DeleteContentItemAsync(id); - Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Document); - await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, true)); - await _hybridCache.RemoveAsync(GetCacheKey(keyAttempt.Result, false)); - _idKeyMap.ClearCache(keyAttempt.Result); - _idKeyMap.ClearCache(id); + await _databaseCacheRepository.DeleteContentItemAsync(content.Id); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, true)); + await _hybridCache.RemoveAsync(GetCacheKey(content.Key, false)); scope.Complete(); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs index 794c22b261..280e0e97f0 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IDocumentCacheService.cs @@ -9,13 +9,13 @@ public interface IDocumentCacheService Task GetByIdAsync(int id, bool preview = false); - Task SeedAsync(IReadOnlyCollection contentTypeKeys); + Task SeedAsync(CancellationToken cancellationToken); Task HasContentByIdAsync(int id, bool preview = false); Task RefreshContentAsync(IContent content); - Task DeleteItemAsync(int id); + Task DeleteItemAsync(IContentBase content); void Rebuild(IReadOnlyCollection contentTypeKeys); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs index ad5ed2d769..bbdf166189 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/IMediaCacheService.cs @@ -13,5 +13,7 @@ public interface IMediaCacheService Task RefreshMediaAsync(IMedia media); - Task DeleteItemAsync(int id); + Task DeleteItemAsync(IContentBase media); + + Task SeedAsync(CancellationToken cancellationToken); } diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs index 9f62072c0d..70f49f9531 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/MediaCacheService.cs @@ -1,4 +1,6 @@ -using Umbraco.Cms.Core; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Scoping; @@ -16,6 +18,29 @@ internal class MediaCacheService : IMediaCacheService private readonly Microsoft.Extensions.Caching.Hybrid.HybridCache _hybridCache; private readonly IPublishedContentFactory _publishedContentFactory; private readonly ICacheNodeFactory _cacheNodeFactory; + private readonly IEnumerable _seedKeyProviders; + private readonly CacheSettings _cacheSettings; + + private HashSet? _seedKeys; + private HashSet SeedKeys + { + get + { + if (_seedKeys is not null) + { + return _seedKeys; + } + + _seedKeys = []; + + foreach (IMediaSeedKeyProvider provider in _seedKeyProviders) + { + _seedKeys.UnionWith(provider.GetSeedKeys()); + } + + return _seedKeys; + } + } public MediaCacheService( IDatabaseCacheRepository databaseCacheRepository, @@ -23,7 +48,9 @@ internal class MediaCacheService : IMediaCacheService ICoreScopeProvider scopeProvider, Microsoft.Extensions.Caching.Hybrid.HybridCache hybridCache, IPublishedContentFactory publishedContentFactory, - ICacheNodeFactory cacheNodeFactory) + ICacheNodeFactory cacheNodeFactory, + IEnumerable seedKeyProviders, + IOptions cacheSettings) { _databaseCacheRepository = databaseCacheRepository; _idKeyMap = idKeyMap; @@ -31,6 +58,8 @@ internal class MediaCacheService : IMediaCacheService _hybridCache = hybridCache; _publishedContentFactory = publishedContentFactory; _cacheNodeFactory = cacheNodeFactory; + _seedKeyProviders = seedKeyProviders; + _cacheSettings = cacheSettings.Value; } public async Task GetByKeyAsync(Guid key) @@ -100,21 +129,45 @@ internal class MediaCacheService : IMediaCacheService scope.Complete(); } - public async Task DeleteItemAsync(int id) + public async Task DeleteItemAsync(IContentBase media) { using ICoreScope scope = _scopeProvider.CreateCoreScope(); - await _databaseCacheRepository.DeleteContentItemAsync(id); - Attempt keyAttempt = _idKeyMap.GetKeyForId(id, UmbracoObjectTypes.Media); - if (keyAttempt.Success) - { - await _hybridCache.RemoveAsync(keyAttempt.Result.ToString()); - } + await _databaseCacheRepository.DeleteContentItemAsync(media.Id); + await _hybridCache.RemoveAsync(media.Key.ToString()); + scope.Complete(); + } - _idKeyMap.ClearCache(keyAttempt.Result); - _idKeyMap.ClearCache(id); + public async Task SeedAsync(CancellationToken cancellationToken) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(); + + foreach (Guid key in SeedKeys) + { + if(cancellationToken.IsCancellationRequested) + { + break; + } + + var cacheKey = GetCacheKey(key, false); + + ContentCacheNode? cachedValue = await _hybridCache.GetOrCreateAsync( + cacheKey, + async cancel => await _databaseCacheRepository.GetMediaSourceAsync(key), + GetSeedEntryOptions()); + + if (cachedValue is null) + { + await _hybridCache.RemoveAsync(cacheKey); + } + } scope.Complete(); } + private HybridCacheEntryOptions GetSeedEntryOptions() => new() + { + Expiration = _cacheSettings.SeedCacheDuration, LocalCacheExpiration = _cacheSettings.SeedCacheDuration, + }; + private string GetCacheKey(Guid key, bool preview) => preview ? $"{key}+draft" : $"{key}"; } diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj index 41fb4becbc..6068233712 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj +++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj @@ -25,6 +25,9 @@ <_Parameter1>DynamicProxyGenAssembly2 + + <_Parameter1>Umbraco.Tests.UnitTests + diff --git a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs index 92f65bbc39..069a0d82b2 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentEditingBuilder.cs @@ -1,7 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; @@ -17,10 +16,10 @@ public class ContentEditingBuilder IWithKeyBuilder, IWithContentTypeKeyBuilder, IWithParentKeyBuilder, - IWithTemplateKeyBuilder + IWithTemplateKeyBuilder, + IBuildContentTypes { - private IContentType _contentType; - private ContentTypeBuilder _contentTypeBuilder; + private ContentTypeEditingBuilder _contentTypeEditingBuilder; private IEnumerable _invariantProperties = []; private IEnumerable _variants = []; private Guid _contentTypeKey; @@ -84,8 +83,7 @@ public class ContentEditingBuilder return this; } - public ContentEditingBuilder AddVariant(string culture, string segment, string name, - IEnumerable properties) + public ContentEditingBuilder AddVariant(string culture, string segment, string name, IEnumerable properties) { var variant = new VariantModel { Culture = culture, Segment = segment, Name = name, Properties = properties }; _variants = _variants.Concat(new[] { variant }); @@ -104,13 +102,6 @@ public class ContentEditingBuilder return this; } - public ContentEditingBuilder WithContentType(IContentType contentType) - { - _contentTypeBuilder = null; - _contentType = contentType; - return this; - } - public override ContentCreateModel Build() { var key = _key ?? Guid.NewGuid(); @@ -120,15 +111,7 @@ public class ContentEditingBuilder var invariantProperties = _invariantProperties; var variants = _variants; - if (_contentTypeBuilder is null && _contentType is null) - { - throw new InvalidOperationException( - "A content item cannot be constructed without providing a content type. Use AddContentType() or WithContentType()."); - } - - var contentType = _contentType ?? _contentTypeBuilder.Build(); var content = new ContentCreateModel(); - content.InvariantName = invariantName; if (parentKey is not null) { @@ -140,7 +123,7 @@ public class ContentEditingBuilder content.TemplateKey = templateKey; } - content.ContentTypeKey = contentType.Key; + content.ContentTypeKey = _contentTypeKey; content.Key = key; content.InvariantProperties = invariantProperties; content.Variants = variants; @@ -148,25 +131,39 @@ public class ContentEditingBuilder return content; } - public static ContentCreateModel CreateBasicContent(IContentType contentType, Guid? key) => + public static ContentCreateModel CreateBasicContent(Guid contentTypeKey, Guid? key) => new ContentEditingBuilder() .WithKey(key) - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName("Home") .Build(); - public static ContentCreateModel CreateSimpleContent(IContentType contentType) => + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey) => new ContentEditingBuilder() - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName("Home") .WithInvariantProperty("title", "Welcome to our Home page") .Build(); - public static ContentCreateModel CreateSimpleContent(IContentType contentType, string name, Guid? parentKey) => + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name, Guid? parentKey) => new ContentEditingBuilder() - .WithContentType(contentType) + .WithContentTypeKey(contentTypeKey) .WithInvariantName(name) .WithParentKey(parentKey) .WithInvariantProperty("title", "Welcome to our Home page") .Build(); + + public static ContentCreateModel CreateSimpleContent(Guid contentTypeKey, string name) => + new ContentEditingBuilder() + .WithContentTypeKey(contentTypeKey) + .WithInvariantName(name) + .WithInvariantProperty("title", "Welcome to our Home page") + .Build(); + + public static ContentCreateModel CreateContentWithTwoVariantProperties(Guid contentTypeKey, string firstCulture, string secondCulture, string propertyAlias, string propertyName) => + new ContentEditingBuilder() + .WithContentTypeKey(contentTypeKey) + .AddVariant(firstCulture, null, firstCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } }) + .AddVariant(secondCulture, null, secondCulture, new[] { new PropertyValueModel { Alias = propertyAlias, Value = propertyName } }) + .Build(); } diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs new file mode 100644 index 0000000000..bcbcbcada0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeEditingBuilder.cs @@ -0,0 +1,240 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class ContentTypeEditingBuilder + : ContentTypeBaseBuilder, + IBuildPropertyTypes +{ + private Guid? _key; + private Guid? _containerKey; + private ContentTypeCleanup _cleanup = new(); + private IEnumerable _allowedTemplateKeys; + private Guid? _defaultTemplateKey; + private bool? _allowAtRoot; + private bool? _isElement; + private bool? _variesByCulture; + private bool? _variesBySegment; + private readonly List _propertyTypeBuilders = []; + private readonly List> _propertyTypeContainerBuilders = []; + private readonly List _allowedContentTypeBuilders = []; + + public ContentTypeEditingBuilder() + : base(null) + { + } + + public ContentTypeEditingBuilder(ContentEditingBuilder parentBuilder) + : base(parentBuilder) + { + } + + public ContentTypeEditingBuilder WithDefaultTemplateKey(Guid templateKey) + { + _defaultTemplateKey = templateKey; + return this; + } + + public ContentTypeEditingBuilder WithIsElement(bool isElement) + { + _isElement = isElement; + return this; + } + + public PropertyTypeContainerBuilder AddPropertyGroup() + { + var builder = new PropertyTypeContainerBuilder(this); + _propertyTypeContainerBuilders.Add(builder); + return builder; + } + + public PropertyTypeEditingBuilder AddPropertyType() + { + var builder = new PropertyTypeEditingBuilder(this); + _propertyTypeBuilders.Add(builder); + return builder; + } + + + public ContentTypeSortBuilder AddAllowedContentType() + { + var builder = new ContentTypeSortBuilder(this); + _allowedContentTypeBuilders.Add(builder); + return builder; + } + + public ContentTypeEditingBuilder AddAllowedTemplateKeys(IEnumerable templateKeys) + { + _allowedTemplateKeys = templateKeys; + return this; + } + + public ContentTypeEditingBuilder WithAllowAtRoot(bool allowAtRoot) + { + _allowAtRoot = allowAtRoot; + return this; + } + + public ContentTypeEditingBuilder WithVariesByCulture(bool variesByCulture) + { + _variesByCulture = variesByCulture; + return this; + } + + public ContentTypeEditingBuilder WithVariesBySegment(bool variesBySegment) + { + _variesBySegment = variesBySegment; + return this; + } + + public override ContentTypeCreateModel Build() + { + ContentTypeCreateModel contentType = new ContentTypeCreateModel(); + contentType.Name = GetName(); + contentType.Alias = GetAlias(); + contentType.Key = GetKey(); + contentType.ContainerKey = _containerKey; + contentType.Cleanup = _cleanup; + contentType.AllowedTemplateKeys = _allowedTemplateKeys ?? Array.Empty(); + contentType.DefaultTemplateKey = _defaultTemplateKey; + contentType.IsElement = _isElement ?? false; + contentType.VariesByCulture = _variesByCulture ?? false; + contentType.VariesBySegment = _variesBySegment ?? false; + contentType.AllowedAsRoot = _allowAtRoot ?? false; + contentType.Properties = _propertyTypeBuilders.Select(x => x.Build()); + contentType.Containers = _propertyTypeContainerBuilders.Select(x => x.Build()); + contentType.AllowedContentTypes = _allowedContentTypeBuilders.Select(x => x.Build()); + + return contentType; + } + + public static ContentTypeCreateModel CreateBasicContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null) + { + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithParentContentType(parent) + .Build(); + } + + public static ContentTypeCreateModel CreateSimpleContentType(string alias = "umbTextpage", string name = "TextPage", IContentType parent = null, string propertyGroupName = "Content", Guid? defaultTemplateKey = null) + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithAllowAtRoot(true) + .WithParentContentType(parent) + .AddPropertyGroup() + .WithKey(containerKey) + .WithName(propertyGroupName) + .Done() + .AddPropertyType() + .WithAlias("title") + .WithDataTypeKey(Constants.DataTypes.Guids.TextareaGuid) + .WithName("Title") + .WithContainerKey(containerKey) + .Done() + .WithDefaultTemplateKey(defaultTemplateKey ?? Guid.Empty) + .AddAllowedTemplateKeys([defaultTemplateKey ?? Guid.Empty]) + .Build(); + } + + public static ContentTypeCreateModel CreateTextPageContentType(string alias = "textPage", string name = "Text Page", Guid defaultTemplateKey = default) + { + var containerKeyOne = Guid.NewGuid(); + var containerKeyTwo = Guid.NewGuid(); + + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithAllowAtRoot(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKeyOne) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithContainerKey(containerKeyOne) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid) + .WithAlias("bodyText") + .WithName("Body text") + .WithContainerKey(containerKeyOne) + .WithSortOrder(2) + .Done() + .AddPropertyGroup() + .WithName("Meta") + .WithSortOrder(2) + .WithKey(containerKeyTwo) + .Done() + .AddPropertyType() + .WithAlias("keywords") + .WithName("Keywords") + .WithContainerKey(containerKeyTwo) + .WithSortOrder(1) + .Done() + .AddPropertyType() + .WithAlias("description") + .WithName("Description") + .WithContainerKey(containerKeyTwo) + .WithSortOrder(2) + .Done() + .AddAllowedTemplateKeys([defaultTemplateKey]) + .WithDefaultTemplateKey(defaultTemplateKey) + .Build(); + } + + public static ContentTypeCreateModel CreateElementType(string alias = "textElement", string name = "Text Element") + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithIsElement(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKey) + .Done() + .AddPropertyType() + .WithDataTypeKey(Constants.DataTypes.Guids.RichtextEditorGuid) + .WithAlias("bodyText") + .WithName("Body text") + .WithContainerKey(containerKey) + .Done() + .Build(); + } + + public static ContentTypeCreateModel CreateContentTypeWithDataTypeKey(Guid dataTypeKey, string alias = "textElement", string name = "Text Element" ) + { + var containerKey = Guid.NewGuid(); + var builder = new ContentTypeEditingBuilder(); + return (ContentTypeCreateModel)builder + .WithAlias(alias) + .WithName(name) + .WithIsElement(true) + .AddPropertyGroup() + .WithName("Content") + .WithKey(containerKey) + .Done() + .AddPropertyType() + .WithDataTypeKey(dataTypeKey) + .WithAlias("dataType") + .WithName("Data Type") + .WithContainerKey(containerKey) + .Done() + .Build(); + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs index 7a4deca5f7..a63205d4d2 100644 --- a/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/ContentTypeSortBuilder.cs @@ -28,6 +28,11 @@ public class ContentTypeSortBuilder { } + public ContentTypeSortBuilder(ContentTypeEditingBuilder parentBuilder) + : base(null) + { + } + string IWithAliasBuilder.Alias { get => _alias; diff --git a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs index 1a4660ed64..f4cc7db311 100644 --- a/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs +++ b/tests/Umbraco.Tests.Common/Builders/Extensions/BuilderExtensions.cs @@ -80,6 +80,13 @@ public static class BuilderExtensions return builder; } + public static T WithDataTypeKey(this T builder, Guid key) + where T : IWithDataTypeKeyBuilder + { + builder.DataTypeKey = key; + return builder; + } + public static T WithParentId(this T builder, int parentId) where T : IWithParentIdBuilder { diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs new file mode 100644 index 0000000000..cc184eea48 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWIthContainerKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWIthContainerKeyBuilder +{ + Guid? ContainerKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs new file mode 100644 index 0000000000..ff188b8017 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithDataTypeKeyBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithDataTypeKeyBuilder +{ + Guid? DataTypeKey { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs new file mode 100644 index 0000000000..e86c18a1dc --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithLabelOnTop.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithLabelOnTop +{ + public bool? LabelOnTop { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs new file mode 100644 index 0000000000..04bea1ecb9 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithMandatoryBuilder +{ + bool? Mandatory { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs new file mode 100644 index 0000000000..0f5c510dc0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithMandatoryMessageBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithMandatoryMessageBuilder +{ + string MandatoryMessage { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs new file mode 100644 index 0000000000..b6cdc81651 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithRegularExpressionBuilder +{ + string RegularExpression { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs new file mode 100644 index 0000000000..6d2e0eef66 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithRegularExpressionMessage.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithRegularExpressionMessage +{ + string RegularExpressionMessage { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs new file mode 100644 index 0000000000..8d25ce8a55 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithTypeBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithTypeBuilder +{ + public string Type { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs new file mode 100644 index 0000000000..fe347f30c0 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesByCultureBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithVariesByCultureBuilder +{ + bool VariesByCulture { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs new file mode 100644 index 0000000000..0d9ea748e1 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/Interfaces/IWithVariesBySegmentBuilder.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Tests.Common.Builders.Interfaces; + +public interface IWithVariesBySegmentBuilder +{ + bool VariesBySegment { get; set; } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs new file mode 100644 index 0000000000..4917b2861b --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeAppearanceBuilder.cs @@ -0,0 +1,22 @@ +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeAppearanceBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithLabelOnTop +{ + private bool? _labelOnTop; + + public PropertyTypeAppearanceBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + } + + bool? IWithLabelOnTop.LabelOnTop + { + get => _labelOnTop; + set => _labelOnTop = value; + } + + public override PropertyTypeAppearance Build() => new() { LabelOnTop = _labelOnTop ?? false }; +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs new file mode 100644 index 0000000000..b5af22eb50 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeContainerBuilder.cs @@ -0,0 +1,66 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeContainerBuilder(TParent parentBuilder) + : ChildBuilderBase(parentBuilder), + IBuildPropertyTypes, IWithKeyBuilder, IWithParentKeyBuilder, IWithNameBuilder, IWithTypeBuilder, + IWithSortOrderBuilder +{ + private Guid? _key; + private Guid? _parentKey; + private string _name; + private string _type; + private int? _sortOrder; + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + Guid? IWithParentKeyBuilder.ParentKey + { + get => _parentKey; + set => _parentKey = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + string IWithTypeBuilder.Type + { + get => _type; + set => _type = value; + } + + int? IWithSortOrderBuilder.SortOrder + { + get => _sortOrder; + set => _sortOrder = value; + } + + public override ContentTypePropertyContainerModel Build() + { + var key = _key ?? Guid.NewGuid(); + var parentKey = _parentKey; + var name = _name ?? "Container"; + var type = _type ?? "Group"; + var sortOrder = _sortOrder ?? 0; + + + return new ContentTypePropertyContainerModel + { + Key = key, + ParentKey = parentKey, + Name = name, + Type = type, + SortOrder = sortOrder, + }; + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs new file mode 100644 index 0000000000..301d77f37b --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeEditingBuilder.cs @@ -0,0 +1,176 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeEditingBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithKeyBuilder, + IWIthContainerKeyBuilder, + IWithSortOrderBuilder, IWithAliasBuilder, IWithNameBuilder, IWithDescriptionBuilder, IWithDataTypeKeyBuilder, + IWithVariesByCultureBuilder, IWithVariesBySegmentBuilder +{ + private Guid? _key; + private Guid? _containerKey; + private int? _sortOrder; + private string _alias; + private string? _name; + private string? _description; + private Guid? _dataTypeKey; + private bool _variesByCulture; + private bool _variesBySegment; + private PropertyTypeValidationEditingBuilder _validationBuilder; + private PropertyTypeAppearanceBuilder _appearanceBuilder; + + public PropertyTypeEditingBuilder(ContentTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + _validationBuilder = new PropertyTypeValidationEditingBuilder(this); + _appearanceBuilder = new PropertyTypeAppearanceBuilder(this); + } + + Guid? IWithKeyBuilder.Key + { + get => _key; + set => _key = value; + } + + Guid? IWIthContainerKeyBuilder.ContainerKey + { + get => _containerKey; + set => _containerKey = value; + } + + int? IWithSortOrderBuilder.SortOrder + { + get => _sortOrder; + set => _sortOrder = value; + } + + string IWithAliasBuilder.Alias + { + get => _alias; + set => _alias = value; + } + + string IWithNameBuilder.Name + { + get => _name; + set => _name = value; + } + + string IWithDescriptionBuilder.Description + { + get => _description; + set => _description = value; + } + + Guid? IWithDataTypeKeyBuilder.DataTypeKey + { + get => _dataTypeKey; + set => _dataTypeKey = value; + } + + bool IWithVariesByCultureBuilder.VariesByCulture + { + get => _variesByCulture; + set => _variesByCulture = value; + } + + bool IWithVariesBySegmentBuilder.VariesBySegment + { + get => _variesBySegment; + set => _variesBySegment = value; + } + + public PropertyTypeValidationEditingBuilder AddValidation() + { + var builder = new PropertyTypeValidationEditingBuilder(this); + _validationBuilder = builder; + return builder; + } + + public PropertyTypeAppearanceBuilder AddAppearance() + { + var builder = new PropertyTypeAppearanceBuilder(this); + _appearanceBuilder = builder; + return builder; + } + + public PropertyTypeEditingBuilder WithContainerKey(Guid? containerKey) + { + _containerKey = containerKey; + return this; + } + + public PropertyTypeEditingBuilder WithSortOrder(int sortOrder) + { + _sortOrder = sortOrder; + return this; + } + + public PropertyTypeEditingBuilder WithAlias(string alias) + { + _alias = alias; + return this; + } + + public PropertyTypeEditingBuilder WithName(string name) + { + _name = name; + return this; + } + + public PropertyTypeEditingBuilder WithDescription(string description) + { + _description = description; + return this; + } + + public PropertyTypeEditingBuilder WithDataTypeKey(Guid dataTypeKey) + { + _dataTypeKey = dataTypeKey; + return this; + } + + public PropertyTypeEditingBuilder WithVariesByCulture(bool variesByCulture) + { + _variesByCulture = variesByCulture; + return this; + } + + public PropertyTypeEditingBuilder WithVariesBySegment(bool variesBySegment) + { + _variesBySegment = variesBySegment; + return this; + } + + public override ContentTypePropertyTypeModel Build() + { + var key = _key ?? Guid.NewGuid(); + var containerKey = _containerKey; + var sortOrder = _sortOrder ?? 0; + var alias = _alias ?? "title"; + var name = _name ?? "Title"; + var description = _description; + var dataTypeKey = _dataTypeKey ?? Constants.DataTypes.Guids.TextareaGuid; + var variesByCulture = _variesByCulture; + var variesBySegment = _variesBySegment; + var validation = _validationBuilder.Build(); + var appearance = _appearanceBuilder.Build(); + + return new ContentTypePropertyTypeModel + { + Key = key, + ContainerKey = containerKey, + SortOrder = sortOrder, + Alias = alias, + Name = name, + Description = description, + DataTypeKey = dataTypeKey, + VariesByCulture = variesByCulture, + VariesBySegment = variesBySegment, + Validation = validation, + Appearance = appearance, + }; + } +} diff --git a/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs new file mode 100644 index 0000000000..781463c760 --- /dev/null +++ b/tests/Umbraco.Tests.Common/Builders/PropertyTypeValidationEditingBuilder.cs @@ -0,0 +1,55 @@ +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Tests.Common.Builders.Interfaces; + +namespace Umbraco.Cms.Tests.Common.Builders; + +public class PropertyTypeValidationEditingBuilder + : ChildBuilderBase, IBuildPropertyTypes, IWithMandatoryBuilder, + IWithMandatoryMessageBuilder, IWithRegularExpressionBuilder, IWithRegularExpressionMessage +{ + private bool? _mandatory; + private string? _mandatoryMessage; + private string? _regularExpression; + private string? _regularExpressionMessage; + + public PropertyTypeValidationEditingBuilder(PropertyTypeEditingBuilder parentBuilder) : base(parentBuilder) + { + } + + bool? IWithMandatoryBuilder.Mandatory + { + get => _mandatory; + set => _mandatory = value; + } + + string? IWithMandatoryMessageBuilder.MandatoryMessage + { + get => _mandatoryMessage; + set => _mandatoryMessage = value; + } + + string? IWithRegularExpressionBuilder.RegularExpression + { + get => _regularExpression; + set => _regularExpression = value; + } + + string? IWithRegularExpressionMessage.RegularExpressionMessage + { + get => _regularExpressionMessage; + set => _regularExpressionMessage = value; + } + + public override PropertyTypeValidation Build() + { + var validation = new PropertyTypeValidation + { + Mandatory = _mandatory ?? false, + MandatoryMessage = _mandatoryMessage ?? null, + RegularExpression = _regularExpression ?? null, + RegularExpressionMessage = _regularExpressionMessage ?? null, + }; + + return validation; + } +} diff --git a/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs new file mode 100644 index 0000000000..d2d083cde7 --- /dev/null +++ b/tests/Umbraco.Tests.Common/TestHelpers/ContentTypeUpdateHelper.cs @@ -0,0 +1,83 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Common.TestHelpers; + +public class ContentTypeUpdateHelper +{ + public ContentTypeUpdateModel CreateContentTypeUpdateModel(IContentType contentType) + { + var updateModel = new ContentTypeUpdateModel(); + var model = MapBaseProperties(contentType, updateModel); + return model; + } + + private T MapBaseProperties(IContentType contentType, T model) where T : ContentTypeModelBase + { + model.Alias = contentType.Alias; + model.Name = contentType.Name; + model.Description = contentType.Description; + model.Icon = contentType.Icon; + model.AllowedAsRoot = contentType.AllowedAsRoot; + model.VariesByCulture = contentType.VariesByCulture(); + model.VariesBySegment = contentType.VariesBySegment(); + model.IsElement = contentType.IsElement; + model.ListView = contentType.ListView; + model.Cleanup = new ContentTypeCleanup() + { + PreventCleanup = contentType.HistoryCleanup.PreventCleanup, + KeepAllVersionsNewerThanDays = contentType.HistoryCleanup.KeepAllVersionsNewerThanDays, + KeepLatestVersionPerDayForDays = contentType.HistoryCleanup.KeepLatestVersionPerDayForDays + }; + + model.AllowedTemplateKeys = contentType.AllowedTemplates.Select(x => x.Key); + model.DefaultTemplateKey = contentType.DefaultTemplate?.Key; + + var tempContainerList = model.Containers.ToList(); + + foreach (var container in contentType.PropertyGroups) + { + var containerModel = new ContentTypePropertyContainerModel() + { + Key = container.Key, + Name = container.Name, + SortOrder = container.SortOrder, + Type = container.Type.ToString() + }; + tempContainerList.Add(containerModel); + } + + model.Containers = tempContainerList.AsEnumerable(); + + var tempPropertyList = model.Properties.ToList(); + + foreach (var propertyType in contentType.PropertyTypes) + { + var propertyModel = new ContentTypePropertyTypeModel + { + Key = propertyType.Key, + ContainerKey = contentType.PropertyGroups.Single(x => x.PropertyTypes.Contains(propertyType)).Key, + SortOrder = propertyType.SortOrder, + Alias = propertyType.Alias, + Name = propertyType.Name, + Description = propertyType.Description, + DataTypeKey = propertyType.DataTypeKey, + VariesByCulture = propertyType.VariesByCulture(), + VariesBySegment = propertyType.VariesBySegment(), + Validation = new PropertyTypeValidation() + { + Mandatory = propertyType.Mandatory, + MandatoryMessage = propertyType.ValidationRegExp, + RegularExpression = propertyType.ValidationRegExp, + RegularExpressionMessage = propertyType.ValidationRegExpMessage, + }, + Appearance = new PropertyTypeAppearance() { LabelOnTop = propertyType.LabelOnTop, } + }; + tempPropertyList.Add(propertyModel); + } + + model.Properties = tempPropertyList.AsEnumerable(); + return model; + } +} diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs index 10dd0cb467..e1f047dd90 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContentEditing.cs @@ -6,28 +6,31 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.ContentTypeEditing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.TestHelpers; namespace Umbraco.Cms.Tests.Integration.Testing; public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrationTest { - protected IContentTypeService ContentTypeService => GetRequiredService(); + protected IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); protected ITemplateService TemplateService => GetRequiredService(); - private ContentEditingService ContentEditingService => - (ContentEditingService)GetRequiredService(); + private IContentEditingService ContentEditingService => (IContentEditingService)GetRequiredService(); - private ContentPublishingService ContentPublishingService => - (ContentPublishingService)GetRequiredService(); + private IContentPublishingService ContentPublishingService => (IContentPublishingService)GetRequiredService(); + protected int TemplateId { get; private set; } + + protected ContentCreateModel Subpage1 { get; private set; } protected ContentCreateModel Subpage2 { get; private set; } - protected ContentCreateModel Subpage3 { get; private set; } - protected ContentCreateModel Subpage { get; private set; } + protected ContentCreateModel PublishedTextPage { get; private set; } protected ContentCreateModel Textpage { get; private set; } @@ -37,13 +40,17 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat protected int TextpageId { get; private set; } - protected int SubpageId { get; private set; } + protected int PublishedTextPageId { get; private set; } + + protected int Subpage1Id { get; private set; } protected int Subpage2Id { get; private set; } - protected int Subpage3Id { get; private set; } + protected ContentTypeCreateModel ContentTypeCreateModel { get; private set; } - protected ContentType ContentType { get; private set; } + protected ContentTypeUpdateModel ContentTypeUpdateModel { get; private set; } + + protected IContentType ContentType { get; private set; } [SetUp] public new void Setup() => CreateTestData(); @@ -53,19 +60,24 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - + TemplateId = template.Id; // Create and Save ContentType "umbTextpage" -> 1051 (template), 1052 (content type) - ContentType = - ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateId: template.Id); - ContentType.Key = new Guid("1D3A8E6E-2EA9-4CC1-B229-1AEE19821522"); - ContentType.AllowedAsRoot = true; - ContentType.AllowedContentTypes = new[] { new ContentTypeSort(ContentType.Key, 0, ContentType.Alias) }; - var contentTypeResult = await ContentTypeService.CreateAsync(ContentType, Constants.Security.SuperUserKey); - Assert.IsTrue(contentTypeResult.Success); + ContentTypeCreateModel = ContentTypeEditingBuilder.CreateSimpleContentType("umbTextpage", "Textpage", defaultTemplateKey: template.Key); + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(contentTypeAttempt.Success); + + var contentTypeResult = contentTypeAttempt.Result; + ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper(); + ContentTypeUpdateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(contentTypeResult); ContentTypeUpdateModel.AllowedContentTypes = new[] + { + new ContentTypeSort(contentTypeResult.Key, 0, ContentTypeCreateModel.Alias), + }; + var updatedContentTypeResult = await ContentTypeEditingService.UpdateAsync(contentTypeResult, ContentTypeUpdateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(updatedContentTypeResult.Success); + ContentType = updatedContentTypeResult.Result; // Create and Save Content "Homepage" based on "umbTextpage" -> 1053 - Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType); - Textpage.Key = new Guid("B58B3AD4-62C2-4E27-B1BE-837BD7C533E0"); + Textpage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key); var createContentResultTextPage = await ContentEditingService.CreateAsync(Textpage, Constants.Security.SuperUserKey); Assert.IsTrue(createContentResultTextPage.Success); @@ -87,24 +99,38 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat }; // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1054 - Subpage = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 1", Textpage.Key); - var createContentResultSubPage = await ContentEditingService.CreateAsync(Subpage, Constants.Security.SuperUserKey); - Assert.IsTrue(createContentResultSubPage.Success); + PublishedTextPage = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Published Page"); + var createContentResultPublishPage = await ContentEditingService.CreateAsync(PublishedTextPage, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultPublishPage.Success); - if (!Subpage.Key.HasValue) + if (!PublishedTextPage.Key.HasValue) { throw new InvalidOperationException("The content page key is null."); } - if (createContentResultSubPage.Result.Content != null) + if (createContentResultPublishPage.Result.Content != null) { - SubpageId = createContentResultSubPage.Result.Content.Id; + PublishedTextPageId = createContentResultPublishPage.Result.Content.Id; } - await ContentPublishingService.PublishAsync(Subpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + var publishResult = await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + Assert.IsTrue(publishResult.Success); // Create and Save Content "Text Page 1" based on "umbTextpage" -> 1055 - Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 2", Textpage.Key); + Subpage1 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 1", Textpage.Key); + var createContentResultSubPage1 = await ContentEditingService.CreateAsync(Subpage1, Constants.Security.SuperUserKey); + Assert.IsTrue(createContentResultSubPage1.Success); + if (!Subpage1.Key.HasValue) + { + throw new InvalidOperationException("The content page key is null."); + } + + if (createContentResultSubPage1.Result.Content != null) + { + Subpage1Id = createContentResultSubPage1.Result.Content.Id; + } + + Subpage2 = ContentEditingBuilder.CreateSimpleContent(ContentType.Key, "Text Page 2", Textpage.Key); var createContentResultSubPage2 = await ContentEditingService.CreateAsync(Subpage2, Constants.Security.SuperUserKey); Assert.IsTrue(createContentResultSubPage2.Success); if (!Subpage2.Key.HasValue) @@ -116,18 +142,5 @@ public abstract class UmbracoIntegrationTestWithContentEditing : UmbracoIntegrat { Subpage2Id = createContentResultSubPage2.Result.Content.Id; } - - Subpage3 = ContentEditingBuilder.CreateSimpleContent(ContentType, "Text Page 3", Textpage.Key); - var createContentResultSubPage3 = await ContentEditingService.CreateAsync(Subpage3, Constants.Security.SuperUserKey); - Assert.IsTrue(createContentResultSubPage3.Success); - if (!Subpage3.Key.HasValue) - { - throw new InvalidOperationException("The content page key is null."); - } - - if (createContentResultSubPage3.Result.Content != null) - { - Subpage3Id = createContentResultSubPage3.Result.Content.Id; - } } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs new file mode 100644 index 0000000000..7c4b62f2af --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Cache/PublishedContentTypeCacheTests.cs @@ -0,0 +1,63 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentTypeEditing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Cache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class PublishedContentTypeCacheTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService(); + private IContentTypeService ContentTypeService => GetRequiredService(); + + [Test] + public async Task Can_Get_Published_DocumentType_By_Key() + { + // Act + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + + // Assert + Assert.IsNotNull(contentType); + } + + [Test] + public async Task Can_Get_Updated_Published_DocumentType_By_Key() + { + // Arrange + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); + Assert.IsNotNull(contentType); + Assert.AreEqual(1, ContentType.PropertyTypes.Count()); + // Update the content type + ContentTypeUpdateHelper contentTypeUpdateHelper = new ContentTypeUpdateHelper(); + var updateModel = contentTypeUpdateHelper.CreateContentTypeUpdateModel(ContentType); + updateModel.Properties = new List(); + await ContentTypeEditingService.UpdateAsync(ContentType, updateModel, Constants.Security.SuperUserKey); + + // Act + var updatedContentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + + // Assert + Assert.IsNotNull(updatedContentType); + Assert.AreEqual(0, updatedContentType.PropertyTypes.Count()); + } + + [Test] + public async Task Published_DocumentType_Gets_Deleted() + { + var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key); + Assert.IsNotNull(contentType); + + await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey); + Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, ContentType.Key)); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs index f0c70c5911..57503c71f2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheDocumentTypeTests.cs @@ -1,7 +1,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -16,16 +16,16 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); - private IPublishedContentTypeCache PublishedContentTypeCache => GetRequiredService(); + private IContentTypeService ContentTypeService => GetRequiredService(); [Test] public async Task Can_Get_Draft_Content_By_Id() { //Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); ContentType.RemovePropertyType("title"); - ContentTypeService.Save(ContentType); + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); // Assert var newTextPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); @@ -36,10 +36,10 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo public async Task Can_Get_Draft_Content_By_Key() { // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); ContentType.RemovePropertyType("title"); - ContentTypeService.Save(ContentType); + await ContentTypeService.UpdateAsync(ContentType, Constants.Security.SuperUserKey); //Assert var newTextPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); Assert.IsNull(newTextPage.Value("title")); @@ -57,26 +57,4 @@ public class DocumentHybridCacheDocumentTypeTests : UmbracoIntegrationTestWithCo var textpageAgain = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview: true); Assert.IsNull(textpageAgain); } - - - // TODO: Copy this into PublishedContentTypeCache - [Test] - public async Task Can_Get_Published_DocumentType_By_Key() - { - var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - var contentTypeAgain = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - } - - [Test] - public async Task Published_DocumentType_Gets_Deleted() - { - var contentType = PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey); - Assert.IsNotNull(contentType); - - await ContentTypeService.DeleteAsync(contentType.Key, Constants.Security.SuperUserKey); - // PublishedContentTypeCache just explodes if it doesn't exist - Assert.Catch(() => PublishedContentTypeCache.Get(PublishedItemType.Content, Textpage.ContentTypeKey)); - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs index ac7c55604f..5fc467f2f6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -1,4 +1,5 @@ -using Moq; +using Microsoft.Extensions.Options; +using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -7,9 +8,11 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Cms.Infrastructure.HybridCache; using Umbraco.Cms.Infrastructure.HybridCache.Factories; using Umbraco.Cms.Infrastructure.HybridCache.Persistence; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; using Umbraco.Cms.Infrastructure.HybridCache.Services; using Umbraco.Cms.Tests.Common.Testing; using Umbraco.Cms.Tests.Integration.Testing; @@ -29,6 +32,8 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent private IContentPublishingService ContentPublishingService => GetRequiredService(); + private CacheSettings _cacheSettings; + [SetUp] public void SetUp() { @@ -44,33 +49,48 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent false, new Dictionary(), null); - _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), It.IsAny())).ReturnsAsync( - new ContentCacheNode() - { - ContentTypeId = Textpage.ContentTypeId, - CreatorId = Textpage.CreatorId, - CreateDate = Textpage.CreateDate, - Id = Textpage.Id, - Key = Textpage.Key, - SortOrder = 0, - Data = contentData, - IsDraft = true, - }); + + + var draftTestCacheNode = new ContentCacheNode() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = true, + }; + + var publishedTestCacheNode = new ContentCacheNode() + { + ContentTypeId = Textpage.ContentTypeId, + CreatorId = Textpage.CreatorId, + CreateDate = Textpage.CreateDate, + Id = Textpage.Id, + Key = Textpage.Key, + SortOrder = 0, + Data = contentData, + IsDraft = false, + }; + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), true)) + .ReturnsAsync(draftTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false)) + .ReturnsAsync(publishedTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), true)) + .ReturnsAsync(draftTestCacheNode); + + _mockedNucacheRepository.Setup(r => r.GetContentSourceAsync(It.IsAny(), false)) + .ReturnsAsync(publishedTestCacheNode); _mockedNucacheRepository.Setup(r => r.GetContentByContentTypeKey(It.IsAny>())).Returns( new List() { - new() - { - ContentTypeId = Textpage.ContentTypeId, - CreatorId = Textpage.CreatorId, - CreateDate = Textpage.CreateDate, - Id = Textpage.Id, - Key = Textpage.Key, - SortOrder = 0, - Data = contentData, - IsDraft = false, - }, + draftTestCacheNode, }); _mockedNucacheRepository.Setup(r => r.DeleteContentItemAsync(It.IsAny())); @@ -81,11 +101,30 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent GetRequiredService(), GetRequiredService(), GetRequiredService(), - GetRequiredService()); + GetRequiredService(), + GetSeedProviders(), + Options.Create(new CacheSettings())); _mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService()); } + // We want to be able to alter the settings for the providers AFTER the test has started + // So we'll manually create them with a magic options mock. + private IEnumerable GetSeedProviders() + { + _cacheSettings = new CacheSettings(); + _cacheSettings.DocumentBreadthFirstSeedCount = 0; + + var mock = new Mock>(); + mock.Setup(m => m.Value).Returns(() => _cacheSettings); + + return new List + { + new ContentTypeSeedKeyProvider(GetRequiredService(), GetRequiredService(), mock.Object), + new DocumentBreadthFirstKeyProvider(GetRequiredService(), mock.Object), + }; + } + [Test] public async Task Content_Is_Cached_By_Key() { @@ -95,7 +134,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var textPage2 = await _mockedCache.GetByIdAsync(Textpage.Key, true); AssertTextPage(textPage); AssertTextPage(textPage2); - _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); } [Test] @@ -121,9 +160,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); Assert.IsTrue(publishResult.Success); Textpage.Published = true; - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id); AssertTextPage(textPage); @@ -141,9 +181,10 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent var publishResult = await ContentPublishingService.PublishAsync(Textpage.Key, schedule, Constants.Security.SuperUserKey); Assert.IsTrue(publishResult.Success); Textpage.Published = true; - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key); AssertTextPage(textPage); @@ -151,12 +192,13 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent } [Test] - public async Task Content_Is_Not_Seeded_If_Unpublished_By_Id() + public async Task Content_Is_Not_Seeded_If_Unpblished_By_Id() { - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true); AssertTextPage(textPage); @@ -166,13 +208,14 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent [Test] public async Task Content_Is_Not_Seeded_If_Unpublished_By_Key() { - await _mockDocumentCacheService.DeleteItemAsync(Textpage.Id); + _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; + await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(new [] {Textpage.ContentType.Key}); + await _mockDocumentCacheService.SeedAsync(); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true); AssertTextPage(textPage); - _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); + _mockedNucacheRepository.Verify(x => x.GetContentSourceAsync(It.IsAny(), It.IsAny()), Times.Exactly(1)); } private void AssertTextPage(IPublishedContent textPage) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs index 0cfc342917..68eddf35df 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCachePropertyTest.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models.ContentPublishing; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -25,23 +26,25 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest private ITemplateService TemplateService => GetRequiredService(); - private IContentTypeService ContentTypeService => GetRequiredService(); - private IContentEditingService ContentEditingService => GetRequiredService(); - private IContentPublishingService ContentPublishingService => GetRequiredService(); + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + private IContentPublishingService ContentPublishingService => GetRequiredService(); [Test] public async Task Can_Get_Value_From_ContentPicker() { + // Arrange var template = TemplateBuilder.CreateTextPageTemplate(); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - var textPage = await CreateTextPageDocument(template.Id); - var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + var textPage = await CreateTextPageDocument(template.Key); + var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key); + // Act var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + // Assert IPublishedContent contentPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); Assert.AreEqual(textPage.Key, contentPickerValue.Key); Assert.AreEqual(textPage.Id, contentPickerValue.Id); @@ -52,10 +55,11 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest [Test] public async Task Can_Get_Value_From_Updated_ContentPicker() { + // Arrange var template = TemplateBuilder.CreateTextPageTemplate(); await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); - var textPage = await CreateTextPageDocument(template.Id); - var contentPickerDocument = await CreateContentPickerDocument(template.Id, textPage.Key); + var textPage = await CreateTextPageDocument(template.Key); + var contentPickerDocument = await CreateContentPickerDocument(template.Key, textPage.Key); // Get for caching var notUpdatedContent = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); @@ -88,46 +92,42 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest Assert.IsTrue(publishResult); + // Act var contentPickerPage = await CacheManager.Content.GetByIdAsync(contentPickerDocument.Id); + + // Assert IPublishedContent updatedPickerValue = (IPublishedContent)contentPickerPage.Value("contentPicker"); - - Assert.AreEqual(textPage.Key, updatedPickerValue.Key); Assert.AreEqual(textPage.Id, updatedPickerValue.Id); Assert.AreEqual(textPage.Name, updatedPickerValue.Name); Assert.AreEqual("Updated title", updatedPickerValue.Properties.First(x => x.Alias == "title").GetValue()); } - private async Task CreateContentPickerDocument(int templateId, Guid textPageKey) + private async Task CreateContentPickerDocument(Guid templateKey, Guid textPageKey) { - var builder = new ContentTypeBuilder(); - var pickerContentType = (ContentType)builder + var builder = new ContentTypeEditingBuilder(); + var pickerContentType = builder .WithAlias("test") .WithName("TestName") - .AddAllowedTemplate() - .WithId(templateId) - .Done() + .WithAllowAtRoot(true) + .AddAllowedTemplateKeys([templateKey]) .AddPropertyGroup() - .WithName("Content") - .WithSupportsPublishing(true) + .WithName("Content") + .Done() .AddPropertyType() - .WithAlias("contentPicker") - .WithName("Content Picker") - .WithDataTypeId(1046) - .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.ContentPicker) - .WithValueStorageType(ValueStorageType.Integer) - .WithSortOrder(16) - .Done() - .Done() + .WithAlias("contentPicker") + .WithName("Content Picker") + .WithDataTypeKey(Constants.DataTypes.Guids.ContentPickerGuid) + .WithSortOrder(16) + .Done() .Build(); - pickerContentType.AllowedAsRoot = true; - ContentTypeService.Save(pickerContentType); + await ContentTypeEditingService.CreateAsync(pickerContentType, Constants.Security.SuperUserKey); var createOtherModel = new ContentCreateModel { - ContentTypeKey = pickerContentType.Key, + ContentTypeKey = pickerContentType.Key.Value, ParentKey = Constants.System.RootKey, InvariantName = "Test Create", InvariantProperties = new[] { new PropertyValueModel { Alias = "contentPicker", Value = textPageKey }, }, @@ -149,15 +149,14 @@ public class DocumentHybridCachePropertyTest : UmbracoIntegrationTest return result.Result.Content; } - private async Task CreateTextPageDocument(int templateId) + private async Task CreateTextPageDocument(Guid templateKey) { - var textContentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: templateId); - textContentType.AllowedAsRoot = true; - ContentTypeService.Save(textContentType); + var textContentType = ContentTypeEditingBuilder.CreateTextPageContentType(defaultTemplateKey: templateKey); + await ContentTypeEditingService.CreateAsync(textContentType, Constants.Security.SuperUserKey); var createModel = new ContentCreateModel { - ContentTypeKey = textContentType.Key, + ContentTypeKey = textContentType.Key.Value, ParentKey = Constants.System.RootKey, InvariantName = "Root Create", InvariantProperties = new[] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs new file mode 100644 index 0000000000..d7d04b64fb --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTemplateTests.cs @@ -0,0 +1,44 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +[Platform("Linux", Reason = "This uses too much memory when running both caches, should be removed when nuchache is removed")] +public class DocumentHybridCacheTemplateTests : UmbracoIntegrationTestWithContentEditing +{ + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder.AddUmbracoHybridCache(); + + private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); + + private IContentEditingService ContentEditingService => GetRequiredService(); + + [Test] + public async Task Can_Get_Document_After_Removing_Template() + { + // Arrange + var textPageBefore = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + Assert.AreEqual(textPageBefore.TemplateId, TemplateId); + var updateModel = new ContentUpdateModel(); + { + updateModel.TemplateKey = null; + updateModel.InvariantName = textPageBefore.Name; + } + + // Act + var updateContentResult = await ContentEditingService.UpdateAsync(textPageBefore.Key, updateModel, Constants.Security.SuperUserKey); + + // Assert + Assert.AreEqual(updateContentResult.Status, ContentEditingOperationStatus.Success); + var textPageAfter = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + // Should this not be null? + Assert.AreEqual(textPageAfter.TemplateId, null); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs index 7d8d4123e1..bf2bfaddb4 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheTests.cs @@ -25,16 +25,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing private const string NewName = "New Name"; private const string NewTitle = "New Title"; - - // Create CRUD Tests for Content, Also cultures. - [Test] public async Task Can_Get_Draft_Content_By_Id() { - //Act + // Act var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); - //Assert + // Assert AssertTextPage(textPage); } @@ -51,54 +48,42 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing [Test] public async Task Can_Get_Published_Content_By_Id() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); } [Test] public async Task Can_Get_Published_Content_By_Key() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); } [Test] public async Task Can_Get_Draft_Of_Published_Content_By_Id() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); Assert.IsFalse(textPage.IsPublished()); } [Test] public async Task Can_Get_Draft_Of_Published_Content_By_Key() { - // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert - AssertTextPage(textPage); + AssertPublishedTextPage(textPage); Assert.IsFalse(textPage.IsPublished()); } @@ -151,19 +136,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Published_Content_By_Id(bool preview, bool result) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantName = NewName; + PublishedTextPage.InvariantName = NewName; ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.AreEqual(result, NewName.Equals(textPage.Name)); @@ -176,22 +160,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Published_Content_By_Key(bool preview, bool result) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - - Textpage.InvariantName = NewName; - + PublishedTextPage.InvariantName = NewName; ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = NewName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); // Assert Assert.AreEqual(result, NewName.Equals(textPage.Name)); @@ -227,11 +207,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Published_Content_Property_By_Id() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -241,11 +220,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Published_Content_Property_By_Key() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -255,11 +233,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Draft_Of_Published_Content_Property_By_Id() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -269,11 +246,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Draft_Of_Published_Content_Property_By_Key() { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - var titleValue = Textpage.InvariantProperties.First(x => x.Alias == "title").Value; + var titleValue = PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value; // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleValue, textPage.Value("title")); @@ -284,7 +260,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing { // Arrange Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = Textpage.InvariantName, @@ -292,7 +267,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing Variants = Textpage.Variants, TemplateKey = Textpage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act @@ -307,7 +281,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing { // Arrange Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - ContentUpdateModel updateModel = new ContentUpdateModel { InvariantName = Textpage.InvariantName, @@ -315,7 +288,6 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing Variants = Textpage.Variants, TemplateKey = Textpage.TemplateKey, }; - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act @@ -329,21 +301,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Published_Content_Property_By_Id() { // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(NewTitle, textPage.Value("title")); @@ -353,21 +323,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Published_Content_Property_By_Key() { // Arrange - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentPublishingService.PublishAsync(PublishedTextPage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value); // Assert Assert.AreEqual(NewTitle, textPage.Value("title")); @@ -379,21 +347,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Id(bool preview, string titleName) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = NewTitle; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.AreEqual(titleName, textPage.Value("title")); @@ -405,21 +370,18 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Get_Updated_Draft_Of_Published_Content_Property_By_Key(bool preview, string titleName) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - Textpage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; - + PublishedTextPage.InvariantProperties.First(x => x.Alias == "title").Value = titleName; ContentUpdateModel updateModel = new ContentUpdateModel { - InvariantName = Textpage.InvariantName, - InvariantProperties = Textpage.InvariantProperties, - Variants = Textpage.Variants, - TemplateKey = Textpage.TemplateKey, + InvariantName = PublishedTextPage.InvariantName, + InvariantProperties = PublishedTextPage.InvariantProperties, + Variants = PublishedTextPage.Variants, + TemplateKey = PublishedTextPage.TemplateKey, }; - - await ContentEditingService.UpdateAsync(Textpage.Key.Value, updateModel, Constants.Security.SuperUserKey); + await ContentEditingService.UpdateAsync(PublishedTextPage.Key.Value, updateModel, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, true); // Assert Assert.AreEqual(titleName, textPage.Value("title")); @@ -429,12 +391,14 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Content_By_Id() { // Arrange - var content = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + var content = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); Assert.IsNotNull(content); - await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3Id, true); + var textPagePublishedContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, false); + + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); // Assert Assert.IsNull(textPage); @@ -444,11 +408,13 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Content_By_Key() { // Arrange - await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); - var result = await ContentEditingService.DeleteAsync(Subpage3.Key.Value, Constants.Security.SuperUserKey); + await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true); + var hasContent = await PublishedContentHybridCache.GetByIdAsync(Subpage1Id, true); + Assert.IsNotNull(hasContent); + await ContentEditingService.DeleteAsync(Subpage1.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage3.Key.Value, true); + var textPage = await PublishedContentHybridCache.GetByIdAsync(Subpage1.Key.Value, true); // Assert Assert.IsNull(textPage); @@ -460,11 +426,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Published_Content_By_Id(bool preview) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(TextpageId, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPageId, preview); // Assert Assert.IsNull(textPage); @@ -476,11 +441,10 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing public async Task Can_Not_Get_Deleted_Published_Content_By_Key(bool preview) { // Arrange - await ContentPublishingService.PublishAsync(Textpage.Key.Value, CultureAndSchedule, Constants.Security.SuperUserKey); - await ContentEditingService.DeleteAsync(Textpage.Key.Value, Constants.Security.SuperUserKey); + await ContentEditingService.DeleteAsync(PublishedTextPage.Key.Value, Constants.Security.SuperUserKey); // Act - var textPage = await PublishedContentHybridCache.GetByIdAsync(Textpage.Key.Value, preview); + var textPage = await PublishedContentHybridCache.GetByIdAsync(PublishedTextPage.Key.Value, preview); // Assert Assert.IsNull(textPage); @@ -499,6 +463,19 @@ public class DocumentHybridCacheTests : UmbracoIntegrationTestWithContentEditing AssertProperties(Textpage.InvariantProperties, textPage.Properties); } + private void AssertPublishedTextPage(IPublishedContent textPage) + { + Assert.Multiple(() => + { + Assert.IsNotNull(textPage); + Assert.AreEqual(PublishedTextPage.Key, textPage.Key); + Assert.AreEqual(PublishedTextPage.ContentTypeKey, textPage.ContentType.Key); + Assert.AreEqual(PublishedTextPage.InvariantName, textPage.Name); + }); + + AssertProperties(PublishedTextPage.InvariantProperties, textPage.Properties); + } + private void AssertProperties(IEnumerable propertyCollection, IEnumerable publishedProperties) { foreach (var prop in propertyCollection) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs index 8f06be20c3..34e69c0344 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheVariantsTests.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -24,12 +25,12 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest private string _invariantTitleAlias = "invariantTitle"; private string _invariantTitleName = "Invariant Title"; - private IContentTypeService ContentTypeService => GetRequiredService(); - private ILanguageService LanguageService => GetRequiredService(); private IContentEditingService ContentEditingService => GetRequiredService(); + private IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); private IPublishedContentCache PublishedContentHybridCache => GetRequiredService(); @@ -49,31 +50,35 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest var updatedInvariantTitle = "Updated Invariant Title"; var updatedVariantTitle = "Updated Variant Title"; - var updateModel = new ContentUpdateModel { - InvariantProperties = new[] - { - new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } - }, - Variants = new [] + InvariantProperties = + new[] { new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } }, + Variants = new[] { new VariantModel { Culture = _englishIsoCode, Name = "Updated English Name", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } - } + Properties = + new[] + { + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + } + }, }, new VariantModel { Culture = _danishIsoCode, Name = "Updated Danish Name", - Properties = new [] + Properties = new[] { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + }, }, }, }, @@ -100,28 +105,29 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest var updatedInvariantTitle = "Updated Invariant Title"; var updatedVariantTitle = "Updated Invariant Title"; - var updateModel = new ContentUpdateModel { - InvariantProperties = new[] - { - new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } - }, - Variants = new [] + InvariantProperties = + new[] { new PropertyValueModel { Alias = _invariantTitleAlias, Value = updatedInvariantTitle } }, + Variants = new[] { new VariantModel { Culture = _englishIsoCode, Name = "Updated English Name", - Properties = new [] + Properties = new[] { - new PropertyValueModel { Alias = _variantTitleAlias, Value = updatedVariantTitle } - } + new PropertyValueModel + { + Alias = _variantTitleAlias, Value = updatedVariantTitle + }, + }, }, }, }; - var result = await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); + var result = + await ContentEditingService.UpdateAsync(VariantPage.Key, updateModel, Constants.Security.SuperUserKey); Assert.IsTrue(result.Success); // Act @@ -134,59 +140,42 @@ public class DocumentHybridCacheVariantsTests : UmbracoIntegrationTest Assert.AreEqual(_variantTitleName, textPage.Value(_variantTitleAlias, _danishIsoCode)); } - private async Task CreateTestData() { - // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. var language = new LanguageBuilder() .WithCultureInfo(_danishIsoCode) .Build(); - await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey); - var contentType = new ContentTypeBuilder() + var groupKey = Guid.NewGuid(); + var contentType = new ContentTypeEditingBuilder() .WithAlias("cultureVariationTest") .WithName("Culture Variation Test") - .WithContentVariation(ContentVariation.Culture) + .WithAllowAtRoot(true) + .WithVariesByCulture(true) .AddPropertyType() - .WithAlias(_variantTitleAlias) - .WithName(_variantTitleName) - .WithVariations(ContentVariation.Culture) - .Done() + .WithAlias(_variantTitleAlias) + .WithName(_variantTitleName) + .WithVariesByCulture(true) + .WithContainerKey(groupKey) + .Done() .AddPropertyType() - .WithAlias(_invariantTitleAlias) - .WithName(_invariantTitleName) - .WithVariations(ContentVariation.Nothing) - .Done() + .WithAlias(_invariantTitleAlias) + .WithName(_invariantTitleName) + .WithContainerKey(groupKey) + .Done() + .AddPropertyGroup() + .WithName("content") + .WithKey(groupKey) + .Done() .Build(); - contentType.AllowedAsRoot = true; - ContentTypeService.Save(contentType); - var rootContentCreateModel = new ContentCreateModel + var contentTypeAttempt = await ContentTypeEditingService.CreateAsync(contentType, Constants.Security.SuperUserKey); + if (!contentTypeAttempt.Success) { - ContentTypeKey = contentType.Key, - Variants = new[] - { - new VariantModel - { - Culture = "en-US", - Name = "English Page", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } - }, - }, - new VariantModel - { - Culture = "da-DK", - Name = "Danish Page", - Properties = new [] - { - new PropertyValueModel { Alias = _variantTitleAlias, Value = _variantTitleName } - }, - }, - }, - }; + throw new Exception("Failed to create content type"); + } + var rootContentCreateModel = ContentEditingBuilder.CreateContentWithTwoVariantProperties(contentTypeAttempt.Result.Key, "en-US", "da-DK", _variantTitleAlias, _variantTitleName); var result = await ContentEditingService.CreateAsync(rootContentCreateModel, Constants.Security.SuperUserKey); VariantPage = result.Result.Content; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs new file mode 100644 index 0000000000..fef4486863 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.PublishedCache.HybridCache/DocumentBreadthFirstKeyProviderTests.cs @@ -0,0 +1,117 @@ +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Cms.Infrastructure.HybridCache.SeedKeyProviders.Document; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +public class DocumentBreadthFirstKeyProviderTests +{ + + [Test] + public void ZeroSeedCountReturnsZeroKeys() + { + // The structure here doesn't matter greatly, it just matters that there is something. + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(It.IsAny(), out rootChildren)).Returns(true); + + + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 0 }; + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.Zero(result.Count); + } + + [Test] + public void OnlyReturnsKeysUpToSeedCount() + { + // Structure + // Root + // - Child1 + // - Child2 + // - Child3 + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true); + + var expected = 3; + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = expected }; + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.That(result.Count, Is.EqualTo(expected)); + } + + [Test] + public void IsBreadthFirst() + { + // Structure + // Root + // - Child1 + // - GrandChild + // - Child2 + // - Child3 + + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + var child1Key = Guid.NewGuid(); + var grandChildKey = Guid.NewGuid(); + IEnumerable rootKeyList = new List { rootKey }; + IEnumerable rootChildren = new List { child1Key, Guid.NewGuid(), Guid.NewGuid() }; + IEnumerable grandChildren = new List { grandChildKey }; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out rootChildren)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(child1Key, out grandChildren)).Returns(true); + + // This'll get all children but no grandchildren + var cacheSettings = new CacheSettings { DocumentBreadthFirstSeedCount = 4 }; + + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(cacheSettings)); + + var result = sut.GetSeedKeys(); + + Assert.That(result.Contains(grandChildKey), Is.False); + } + + [Test] + public void CanGetAll() + { + var navigationQueryService = new Mock(); + var rootKey = Guid.NewGuid(); + + + IEnumerable rootKeyList = new List { rootKey }; + var childrenCount = 300; + List rootChildren = new List (); + for (int i = 0; i < childrenCount; i++) + { + rootChildren.Add(Guid.NewGuid()); + } + + IEnumerable childrenEnumerable = rootChildren; + navigationQueryService.Setup(x => x.TryGetRootKeys(out rootKeyList)).Returns(true); + navigationQueryService.Setup(x => x.TryGetChildrenKeys(rootKey, out childrenEnumerable)).Returns(true); + var settings = new CacheSettings { DocumentBreadthFirstSeedCount = int.MaxValue }; + + + var sut = new DocumentBreadthFirstKeyProvider(navigationQueryService.Object, Options.Create(settings)); + + var result = sut.GetSeedKeys(); + + var expected = childrenCount + 1; // Root + children + Assert.That(result.Count, Is.EqualTo(expected)); + } +} From 0c88d2f4db1d6bce4f18e3df9dae7297b5aaac01 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:42:28 +0200 Subject: [PATCH 26/38] update lockfile --- src/Umbraco.Web.UI.Login/package-lock.json | 1879 +++++++------------- src/Umbraco.Web.UI.Login/package.json | 3 +- 2 files changed, 637 insertions(+), 1245 deletions(-) diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index 43eaac8a2b..054a6c5be0 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -6,8 +6,7 @@ "": { "name": "login", "devDependencies": { - "@umbraco-cms/backoffice": "^14.0.0", - "@umbraco-ui/uui-css": "^1.8.0", + "@umbraco-cms/backoffice": "^14.2.0", "msw": "^2.3.0", "typescript": "^5.4.5", "vite": "^5.2.11", @@ -20,94 +19,27 @@ }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", - "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", "dev": true, + "license": "ISC", "dependencies": { "cookie": "^0.5.0" } }, "node_modules/@bundled-es-modules/statuses": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", "dev": true, + "license": "ISC", "dependencies": { "statuses": "^2.0.1" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -116,299 +48,10 @@ "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@inquirer/confirm": { "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.10.tgz", - "integrity": "sha512-/aAHu83Njy6yf44T+ZrRPUkMcUqprrOiIKsyMvf9jOV+vF5BNb2ja1aLP33MK36W8eaf91MTL/mU/e6METuENg==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/core": "^8.2.3", "@inquirer/type": "^1.3.3" @@ -419,9 +62,8 @@ }, "node_modules/@inquirer/core": { "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-8.2.3.tgz", - "integrity": "sha512-WrpDVPAaxJQjHid3Ra4FhUO70YBzkHSYVyW5X48L5zHYdudoPISJqTRRWSeamHfaXda7PNNaC5Py5MEo7QwBNA==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/figures": "^1.0.3", "@inquirer/type": "^1.3.3", @@ -443,26 +85,24 @@ }, "node_modules/@inquirer/figures": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", - "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/type": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.3.3.tgz", - "integrity": "sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@lit-labs/ssr-dom-shim": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz", - "integrity": "sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", + "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", "dev": true, "peer": true }, @@ -478,18 +118,16 @@ }, "node_modules/@mswjs/cookies": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.1.tgz", - "integrity": "sha512-W68qOHEjx1iD+4VjQudlx26CPIoxmIAtK4ZCexU0/UJBG6jYhcuyzKJx+Iw8uhBIGd9eba64XgWVgo20it1qwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@mswjs/interceptors": { "version": "0.29.1", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", - "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", "dev": true, + "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", @@ -504,15 +142,13 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "dev": true, + "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" @@ -520,236 +156,36 @@ }, "node_modules/@open-draft/until": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", - "cpu": [ - "arm" - ], "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] + "license": "MIT" }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@types/cookie": { "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/diff": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.1.tgz", - "integrity": "sha512-uxpcuwWJGhe2AR1g8hD9F5OYGCqjqWnBUQFD8gMZsDbv8oPHzxJF6iMO6n8Tk0AdzlxoaaoQhOYlIg/PukVU8g==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@types/dompurify": { "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", - "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "dev": true, + "license": "MIT", "peer": true, "dependencies": { "@types/trusted-types": "*" @@ -757,59 +193,73 @@ }, "node_modules/@types/estree": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mute-stream": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { "version": "20.14.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", - "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/statuses": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", - "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", "dev": true, "peer": true }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@umbraco-cms/backoffice": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@umbraco-cms/backoffice/-/backoffice-14.0.0.tgz", - "integrity": "sha512-PpJXHBeqDEEKTf4L/K7d8Ua9E2G9VuXHL76lBmBrf+1Sa7iWVuqf5BCvSa0wy1hKLrU1ytLsmByvVHEcix4XOw==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/@umbraco-cms/backoffice/-/backoffice-14.2.0.tgz", + "integrity": "sha512-MWnQl9LTVMm+UKf0DW9qtViYJ1C/4ThPef3AP+dy1CfWfJjgFl9ZD0Y1Y8t2c3Rp4BziT68d3YlsL6JNGIqNmw==", "dev": true, + "workspaces": [ + "./src/packages/block", + "./src/packages/code-editor", + "./src/packages/core", + "./src/packages/data-type", + "./src/packages/dictionary", + "./src/packages/documents", + "./src/packages/health-check", + "./src/packages/language", + "./src/packages/media", + "./src/packages/members", + "./src/packages/multi-url-picker", + "./src/packages/property-editors", + "./src/packages/tags", + "./src/packages/templating", + "./src/packages/tiny-mce", + "./src/packages/umbraco-news", + "./src/packages/user", + "./src/packages/webhook" + ], "engines": { "node": ">=20.9 <21", "npm": ">=10.1 < 11" @@ -817,159 +267,159 @@ "peerDependencies": { "@types/diff": "^5.2.1", "@types/dompurify": "^3.0.5", - "@types/uuid": "^9.0.8", - "@umbraco-ui/uui": "1.8.1", - "@umbraco-ui/uui-css": "1.8.0", + "@types/uuid": "^10.0.0", + "@umbraco-ui/uui": "^1.9.0", + "@umbraco-ui/uui-css": "^1.9.0", "base64-js": "^1.5.1", "diff": "^5.2.0", - "dompurify": "^3.1.4", + "dompurify": "^3.1.6", "element-internals-polyfill": "^1.3.11", - "lit": "^3.1.3", - "marked": "^12.0.2", - "monaco-editor": "^0.48.0", + "lit": "^3.1.4", + "marked": "^13.0.2", + "monaco-editor": "^0.50.0", "rxjs": "^7.8.1", "tinymce": "^6.8.3", - "tinymce-i18n": "^24.5.8", - "uuid": "^9.0.1" + "tinymce-i18n": "^24.7.15", + "uuid": "^10.0.0" } }, "node_modules/@umbraco-ui/uui": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.8.1.tgz", - "integrity": "sha512-KWKtuSQdxeCbGH2gezumuFysf+33q2TZy4h85zCABFvHgOeeR1EB7/S2jUNGoe2IOqYLktYl0GdQszfmyFN1dg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui/-/uui-1.10.0.tgz", + "integrity": "sha512-Jkbqcgo78naFjp4/QBfuxqbr7WXFvZHq5RTHNMa7SAzb/EavdWp6mBLL2Txu9259ZASIdnlsuqyrKX6giiY+Kw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-action-bar": "1.8.0", - "@umbraco-ui/uui-avatar": "1.8.0", - "@umbraco-ui/uui-avatar-group": "1.8.0", - "@umbraco-ui/uui-badge": "1.8.0", - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-boolean-input": "1.8.0", - "@umbraco-ui/uui-box": "1.8.0", - "@umbraco-ui/uui-breadcrumbs": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-button-group": "1.8.0", - "@umbraco-ui/uui-button-inline-create": "1.8.0", - "@umbraco-ui/uui-card": "1.8.0", - "@umbraco-ui/uui-card-block-type": "1.8.0", - "@umbraco-ui/uui-card-content-node": "1.8.0", - "@umbraco-ui/uui-card-media": "1.8.0", - "@umbraco-ui/uui-card-user": "1.8.0", - "@umbraco-ui/uui-caret": "1.8.0", - "@umbraco-ui/uui-checkbox": "1.8.0", - "@umbraco-ui/uui-color-area": "1.8.0", - "@umbraco-ui/uui-color-picker": "1.8.0", - "@umbraco-ui/uui-color-slider": "1.8.0", - "@umbraco-ui/uui-color-swatch": "1.8.0", - "@umbraco-ui/uui-color-swatches": "1.8.0", - "@umbraco-ui/uui-combobox": "1.8.0", - "@umbraco-ui/uui-combobox-list": "1.8.0", - "@umbraco-ui/uui-css": "1.8.0", - "@umbraco-ui/uui-dialog": "1.8.0", - "@umbraco-ui/uui-dialog-layout": "1.8.0", - "@umbraco-ui/uui-file-dropzone": "1.8.0", - "@umbraco-ui/uui-file-preview": "1.8.0", - "@umbraco-ui/uui-form": "1.8.0", - "@umbraco-ui/uui-form-layout-item": "1.8.1", - "@umbraco-ui/uui-form-validation-message": "1.8.1", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-icon-registry": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0", - "@umbraco-ui/uui-input": "1.8.0", - "@umbraco-ui/uui-input-file": "1.8.0", - "@umbraco-ui/uui-input-lock": "1.8.0", - "@umbraco-ui/uui-input-password": "1.8.0", - "@umbraco-ui/uui-keyboard-shortcut": "1.8.0", - "@umbraco-ui/uui-label": "1.8.0", - "@umbraco-ui/uui-loader": "1.8.0", - "@umbraco-ui/uui-loader-bar": "1.8.0", - "@umbraco-ui/uui-loader-circle": "1.8.0", - "@umbraco-ui/uui-menu-item": "1.8.0", - "@umbraco-ui/uui-modal": "1.8.0", - "@umbraco-ui/uui-pagination": "1.8.0", - "@umbraco-ui/uui-popover": "1.8.0", - "@umbraco-ui/uui-popover-container": "1.8.0", - "@umbraco-ui/uui-progress-bar": "1.8.0", - "@umbraco-ui/uui-radio": "1.8.0", - "@umbraco-ui/uui-range-slider": "1.8.0", - "@umbraco-ui/uui-ref": "1.8.0", - "@umbraco-ui/uui-ref-list": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0", - "@umbraco-ui/uui-ref-node-data-type": "1.8.0", - "@umbraco-ui/uui-ref-node-document-type": "1.8.0", - "@umbraco-ui/uui-ref-node-form": "1.8.0", - "@umbraco-ui/uui-ref-node-member": "1.8.0", - "@umbraco-ui/uui-ref-node-package": "1.8.0", - "@umbraco-ui/uui-ref-node-user": "1.8.0", - "@umbraco-ui/uui-scroll-container": "1.8.0", - "@umbraco-ui/uui-select": "1.8.0", - "@umbraco-ui/uui-slider": "1.8.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0", - "@umbraco-ui/uui-symbol-file": "1.8.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0", - "@umbraco-ui/uui-symbol-lock": "1.8.0", - "@umbraco-ui/uui-symbol-more": "1.8.0", - "@umbraco-ui/uui-symbol-sort": "1.8.0", - "@umbraco-ui/uui-table": "1.8.0", - "@umbraco-ui/uui-tabs": "1.8.0", - "@umbraco-ui/uui-tag": "1.8.0", - "@umbraco-ui/uui-textarea": "1.8.0", - "@umbraco-ui/uui-toast-notification": "1.8.0", - "@umbraco-ui/uui-toast-notification-container": "1.8.0", - "@umbraco-ui/uui-toast-notification-layout": "1.8.0", - "@umbraco-ui/uui-toggle": "1.8.0", - "@umbraco-ui/uui-visually-hidden": "1.8.0" + "@umbraco-ui/uui-action-bar": "1.10.0", + "@umbraco-ui/uui-avatar": "1.10.0", + "@umbraco-ui/uui-avatar-group": "1.10.0", + "@umbraco-ui/uui-badge": "1.10.0", + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-boolean-input": "1.10.0", + "@umbraco-ui/uui-box": "1.10.0", + "@umbraco-ui/uui-breadcrumbs": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-button-group": "1.10.0", + "@umbraco-ui/uui-button-inline-create": "1.10.0", + "@umbraco-ui/uui-card": "1.10.0", + "@umbraco-ui/uui-card-block-type": "1.10.0", + "@umbraco-ui/uui-card-content-node": "1.10.0", + "@umbraco-ui/uui-card-media": "1.10.0", + "@umbraco-ui/uui-card-user": "1.10.0", + "@umbraco-ui/uui-caret": "1.10.0", + "@umbraco-ui/uui-checkbox": "1.10.0", + "@umbraco-ui/uui-color-area": "1.10.0", + "@umbraco-ui/uui-color-picker": "1.10.0", + "@umbraco-ui/uui-color-slider": "1.10.0", + "@umbraco-ui/uui-color-swatch": "1.10.0", + "@umbraco-ui/uui-color-swatches": "1.10.0", + "@umbraco-ui/uui-combobox": "1.10.0", + "@umbraco-ui/uui-combobox-list": "1.10.0", + "@umbraco-ui/uui-css": "1.10.0", + "@umbraco-ui/uui-dialog": "1.10.0", + "@umbraco-ui/uui-dialog-layout": "1.10.0", + "@umbraco-ui/uui-file-dropzone": "1.10.0", + "@umbraco-ui/uui-file-preview": "1.10.0", + "@umbraco-ui/uui-form": "1.10.0", + "@umbraco-ui/uui-form-layout-item": "1.10.0", + "@umbraco-ui/uui-form-validation-message": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-icon-registry": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0", + "@umbraco-ui/uui-input": "1.10.0", + "@umbraco-ui/uui-input-file": "1.10.0", + "@umbraco-ui/uui-input-lock": "1.10.0", + "@umbraco-ui/uui-input-password": "1.10.0", + "@umbraco-ui/uui-keyboard-shortcut": "1.10.0", + "@umbraco-ui/uui-label": "1.10.0", + "@umbraco-ui/uui-loader": "1.10.0", + "@umbraco-ui/uui-loader-bar": "1.10.0", + "@umbraco-ui/uui-loader-circle": "1.10.0", + "@umbraco-ui/uui-menu-item": "1.10.0", + "@umbraco-ui/uui-modal": "1.10.0", + "@umbraco-ui/uui-pagination": "1.10.0", + "@umbraco-ui/uui-popover": "1.10.0", + "@umbraco-ui/uui-popover-container": "1.10.0", + "@umbraco-ui/uui-progress-bar": "1.10.0", + "@umbraco-ui/uui-radio": "1.10.0", + "@umbraco-ui/uui-range-slider": "1.10.0", + "@umbraco-ui/uui-ref": "1.10.0", + "@umbraco-ui/uui-ref-list": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0", + "@umbraco-ui/uui-ref-node-data-type": "1.10.0", + "@umbraco-ui/uui-ref-node-document-type": "1.10.0", + "@umbraco-ui/uui-ref-node-form": "1.10.0", + "@umbraco-ui/uui-ref-node-member": "1.10.0", + "@umbraco-ui/uui-ref-node-package": "1.10.0", + "@umbraco-ui/uui-ref-node-user": "1.10.0", + "@umbraco-ui/uui-scroll-container": "1.10.0", + "@umbraco-ui/uui-select": "1.10.0", + "@umbraco-ui/uui-slider": "1.10.0", + "@umbraco-ui/uui-symbol-expand": "1.10.0", + "@umbraco-ui/uui-symbol-file": "1.10.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.10.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.10.0", + "@umbraco-ui/uui-symbol-folder": "1.10.0", + "@umbraco-ui/uui-symbol-lock": "1.10.0", + "@umbraco-ui/uui-symbol-more": "1.10.0", + "@umbraco-ui/uui-symbol-sort": "1.10.0", + "@umbraco-ui/uui-table": "1.10.0", + "@umbraco-ui/uui-tabs": "1.10.0", + "@umbraco-ui/uui-tag": "1.10.0", + "@umbraco-ui/uui-textarea": "1.10.0", + "@umbraco-ui/uui-toast-notification": "1.10.0", + "@umbraco-ui/uui-toast-notification-container": "1.10.0", + "@umbraco-ui/uui-toast-notification-layout": "1.10.0", + "@umbraco-ui/uui-toggle": "1.10.0", + "@umbraco-ui/uui-visually-hidden": "1.10.0" } }, "node_modules/@umbraco-ui/uui-action-bar": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.8.0.tgz", - "integrity": "sha512-IRs42chstgXFo5b3i0j80Emt+uZSt/WmDDv7gTtB768FL1C+k0BR5sYVleEmUdkfCOv+WIVo1FAqd+9CPFkDDw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-action-bar/-/uui-action-bar-1.10.0.tgz", + "integrity": "sha512-f4nQx/s4XRtnQA3p/Q+qOhMi5zdK2ZUz7rh0qAT9Qi+Y24uLwnMabLWlnMRqyDe5z5/DTMoYDKaKhRWTjVFjzw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button-group": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button-group": "1.10.0" } }, "node_modules/@umbraco-ui/uui-avatar": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.8.0.tgz", - "integrity": "sha512-ek6SFYEvEbu1Jf1FVrqBDHuWqCnekkU1hm4XDHEpEyhPE5OOC70SyYLB6brT0kvgBE0QKB2txYu7u8ZbWzy+OQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar/-/uui-avatar-1.10.0.tgz", + "integrity": "sha512-JwCoFF/lLkLwRWYRYDoi8w7qHp3bFZQvCu9unQ8QQg/XZVULbiDGwZnSJoMPRdNo6fpN/hx8gDszYDn1tMkCaw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-avatar-group": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.8.0.tgz", - "integrity": "sha512-AS6+GzeoAOS6vuZ6okP30iik8cvYPjBvoWtSYcnV0gScw52FIg9ak+j5L+rQHzE8LCqT8c6RE63HsAsJe7f6oA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-avatar-group/-/uui-avatar-group-1.10.0.tgz", + "integrity": "sha512-JMx0cBIsKfhSSf/I0rBNw3jxjKOMw3UJEDL0YTOEBUhtsVIXf7LSQlJ8977vzJuO7a3PjlhWx8F7lhukRzL3aw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-avatar": "1.8.0", - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-avatar": "1.10.0", + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-badge": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.8.0.tgz", - "integrity": "sha512-mKHkkXIwN7oUybeQo5J5TOgqghinJH5gE9lJwOemNCy/oiV/TeYHOr7MqHxIJ+13Nwl9O6JbSRWbPbOD9TSkVw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-badge/-/uui-badge-1.10.0.tgz", + "integrity": "sha512-x+UsAQknE3kT7yxAhzS38ILqvDzKdEmYxlYES4dqR5Cj/Vc4iMZTY4pRd1UJEBsC5G/tLlquDGbXG9IZCgCk/w==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-base": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.8.0.tgz", - "integrity": "sha512-LIPS3sfgOr/cgpDueTqpX+t6Bw0BpNISQSrAeyC+c6X0WiahKLuwob6UXSnefh9j5xIYa5+GY1gEUDgI4wlRhg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-base/-/uui-base-1.10.0.tgz", + "integrity": "sha512-0t6BpQmXPgSdjDhb3rQEYcJJtkE50w5rZppsOFXyossaBxfijfTK9JRVc95JRzpLn/a5iQyhuxdUx9r85t71HA==", "dev": true, "peer": true, "peerDependencies": { @@ -977,843 +427,843 @@ } }, "node_modules/@umbraco-ui/uui-boolean-input": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.8.0.tgz", - "integrity": "sha512-6GqzuALrzrJIWIAdsYAau9t3sxYL0P+OKSKpcvj+4/DkbhbWjk54CtVFyWBAzYa9LhZHauGl2VYzxSvmGWARSA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-boolean-input/-/uui-boolean-input-1.10.0.tgz", + "integrity": "sha512-Xe+B1E+RJCHmSK3aE/ZHVjZwJkijNOh4Un+x42oZX2XQqTz57aafBVY5HSrY7/N8n0xvSY1HCc6sG9wJyXXfMQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-box": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.8.0.tgz", - "integrity": "sha512-/j/69od/uWd1Utb2IzU5pq5cvKpsq0cV4F+pS9e6HejzpcVUCz1AtdKNQvgpyOzd/oS9r8Z6pYL/V/gEydyqwg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-box/-/uui-box-1.10.0.tgz", + "integrity": "sha512-BF/AUdGTjRxf4OqdVapMkKNkRLbnHTvuYMEhXDxhHT19prB8ZQQLzjDiX1WW9+q8owq3LPVMxoRjT/+AATpsEA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-css": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-css": "1.10.0" } }, "node_modules/@umbraco-ui/uui-breadcrumbs": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.8.0.tgz", - "integrity": "sha512-Hv5NAfRzfaXcDYcuribjpaooZk1LVcHNTaLwoxVAUN64sufYS8kfw0sN8+jsacumUP3rpy0XgR9Ic37JUoIkBw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-breadcrumbs/-/uui-breadcrumbs-1.10.0.tgz", + "integrity": "sha512-yXzTPi/CTb48QQjgFhFUgO5yM2fe/f7gOiPcXKUelLFbCnWV+HpvO+5QdE9fklJd9rTLb7OuxBVsTU96j90fPA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-button": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.8.0.tgz", - "integrity": "sha512-M0pLzpGt2CuzQAHqiHQwVdzFOyb9EznCT7b+YMQOlJCMfeVmN2KBF2xUlfb/3M2LVDukTHyGHzqaWj8Lj6YUbA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button/-/uui-button-1.10.0.tgz", + "integrity": "sha512-zCTVTPMkBG6zQpSzHALOjSlsQu4e1SZCciZoC2bD6aZ6nQbx1C9z8mgIGsNt8lGQqzU5GnF1nVGwIfFn6MEq7Q==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0" } }, "node_modules/@umbraco-ui/uui-button-group": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.8.0.tgz", - "integrity": "sha512-5K/cvrOWvRmoXByuI1illF2e9sCzzegmlEpS4XbVk1XW/6quzRJpXSCrY+awj01kFrxB+UC8mB1DIECHKNyeVg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-group/-/uui-button-group-1.10.0.tgz", + "integrity": "sha512-8Fmrs920fExYhvAvm7LtKfwNqwE4bAjqTeNYUoisthbKcme2//po1w4II6RLYGLfq39A6mcEpn9IptjIsPF67g==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-button-inline-create": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.8.0.tgz", - "integrity": "sha512-smSZKMG0cAp+BTZe0wRaYotcQElja8gqznGaIyuGuWvvpvWEiuWMC9xjMytFNvaawCN1+uLc5fdcCArPmFjH+w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-button-inline-create/-/uui-button-inline-create-1.10.0.tgz", + "integrity": "sha512-MBQLGhBZJzhUxzrFvbAtBZkK5zlZkS1nR1vBTQEqunORjbooo6JEeTBrlPRk7HDPJpWWsqB6uIe0m2UQ8rBAPA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-card": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.8.0.tgz", - "integrity": "sha512-wwHaDbwDmragvIhhAtv/D2Ys47kXFPBKvE+/ahofXUygjTGbmjbKJ57Qfo6x8sD1BM3bSTDU6gUWaf4s0/D3WQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card/-/uui-card-1.10.0.tgz", + "integrity": "sha512-ozWuLFJanivYDybnJgvPomPPwhCxPHg2NatpNGxLnDnn0VCJVwQQ4vvejrjdHTGknjITmFaGweZbJG3a4q+G1g==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-card-block-type": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.8.0.tgz", - "integrity": "sha512-8Ydat2K4LipsQaCEhDTN4DeVHiqOCdEOY4Z43XCf6bTU91lNPGdagtC0ZE9b4M28E03ou4E19Ms7o2m59g0OWw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-block-type/-/uui-card-block-type-1.10.0.tgz", + "integrity": "sha512-9BMH9Z5jS75++onPuGIODcsHFS+bCuEKjgTOqmCDZfU7BIMLMJH/+OW2Uzwqoh+4If41Yumm2TiOSwxo6KOOdw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-card": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-card": "1.10.0" } }, "node_modules/@umbraco-ui/uui-card-content-node": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.8.0.tgz", - "integrity": "sha512-R0SYtKk5Z+on0xQ0cD1z2z43mSTgYi7sKtQDrhwoP/sWbp9xIS2wPOO+PoMiUonwXPo4JuSZ9ghgT4HzsQce1A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-content-node/-/uui-card-content-node-1.10.0.tgz", + "integrity": "sha512-iJQucR2IDC6OQFuCsqHjUlt6ze/X6n0ZQm/nDnSZofVTRRFOJb95T0CA6Ytm4Atuz3K0kkLr4AcKsb10/6Zayw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-card": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-card": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0" } }, "node_modules/@umbraco-ui/uui-card-media": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.8.0.tgz", - "integrity": "sha512-C1xsdO9mWJ/gzE8nLfF2k5NfpFzJ2yMQYzJVtov3s9C33iy6NVq7OM67o+QugCqDuwwYSkonjgNJLHTav78KVg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-media/-/uui-card-media-1.10.0.tgz", + "integrity": "sha512-zWL+/cnQRVFpvWPUOhHjirW9WxBRpC5tFfdE1SunvKBNkKhygGsPTq+b/Te9dI024ZLyaazej57NkpylGeNSOA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-card": "1.8.0", - "@umbraco-ui/uui-symbol-file": "1.8.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-card": "1.10.0", + "@umbraco-ui/uui-symbol-file": "1.10.0", + "@umbraco-ui/uui-symbol-folder": "1.10.0" } }, "node_modules/@umbraco-ui/uui-card-user": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.8.0.tgz", - "integrity": "sha512-b6LiMCl/oFaW6MnPXBMCPqlVDHF/TT3LByQOTJ8rE+WHzY/zE9toVLy/LccxB62Ur/Hz7XakhU8xHaugH+zs3w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-card-user/-/uui-card-user-1.10.0.tgz", + "integrity": "sha512-e6LRpSfgKzbKo2pSKL5Ku9jaB5P6lowiV0/0l/uGHkvXfFfuCUVoMPjQncuCcaMcGW7Q2g5lkXNgOOXtiuyw0A==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-avatar": "1.8.0", - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-card": "1.8.0" + "@umbraco-ui/uui-avatar": "1.10.0", + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-card": "1.10.0" } }, "node_modules/@umbraco-ui/uui-caret": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.8.0.tgz", - "integrity": "sha512-LxS0vLKZYNKsef/FgNXFh/CBauf+6Dgac+n5VyCu6FElh8UTP+NOeAA/4KEVSJh8gThJ0Fmb8qoMSFop+41wLg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-caret/-/uui-caret-1.10.0.tgz", + "integrity": "sha512-XCsvDV5CKZ5wy1zdjEQ849411B/fCkENRmqCiqnHa1+JFAVgbb1AA1+gjb+lz4EWpE1CfiL556mYjt1ZznwFZA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-checkbox": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.8.0.tgz", - "integrity": "sha512-Gf/kZ4q5SuLNEEfcL1/YRzcOI5CZTsMyo2+9LuksCZqRzWtxoo1meB42GZRjE4GTCFZfQOr4Ayz7ZNRaizuM+Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-checkbox/-/uui-checkbox-1.10.0.tgz", + "integrity": "sha512-1NPLxyGGJcWuU0tXzw/FpQrwjEfBzrO4yTkss+kRbryi9yrxJgxfOsug+JIHSEfiFjQoSDU/mvoirPxa5xhGIg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-boolean-input": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-boolean-input": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0" } }, "node_modules/@umbraco-ui/uui-color-area": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.8.0.tgz", - "integrity": "sha512-V3+iph2Vq58T9f4FoJvxsjq0LpH5VJhC2P2VkAWvMO1m528QOULDP+b5csYWH1pF78RxSZ5Lm042Dnw9XOqN2w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-area/-/uui-color-area-1.10.0.tgz", + "integrity": "sha512-zmJseESe9KmFmJrrI+/l1a2RLOZWuRNp8MTjhuaf7p9HBopOeYyhC4vXgf/6VPa+y5uZyitRM6d/yUQmL7CxsA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", + "@umbraco-ui/uui-base": "1.10.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-picker": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.8.0.tgz", - "integrity": "sha512-DQtKN4ivIRx54SYUhxdlbFf5ibmnp5/mscOiC98HObYVTeM9ZJUrKfFGTU9Qfekz3q+rPzzv7eec8E0Zb6qfwg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-picker/-/uui-color-picker-1.10.0.tgz", + "integrity": "sha512-MYDaO+pBUTH7lpdjH5RQivqEc4JqFcpTD0qEqyk7iyU5vHJ7HcYLng5fiNuEhavVGhN6f0Ee10bAq7cWx3ZKyA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-popover-container": "1.8.0", + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-popover-container": "1.10.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-slider": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.8.0.tgz", - "integrity": "sha512-DHCFX0JZXqI6wp2fRe+lOd9TAJVzTC9ghDNP+qMGattsxRnTf/pxoYucW7F5ee/ggiBIsS8i47kFa2wDmausiA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-slider/-/uui-color-slider-1.10.0.tgz", + "integrity": "sha512-Y6u5G7YVHMVC28rmggFYGSdB3A3MO6wZ2GL1YJjzzO8smRRAjtYkOkkbHYSKgn4Mao9K2BHn4DuZVizhWQe8Aw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-color-swatch": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.8.0.tgz", - "integrity": "sha512-z9RQ0R5E9SErqiY1/kU6Rnp+LQBM119OKqAexHo32cM/9iyzLIEUY4CwzCYE9/ogeXDgljXLTGX21jddCNCv9A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatch/-/uui-color-swatch-1.10.0.tgz", + "integrity": "sha512-BazYXqGeScvYsHuOZlnT0Yi8xRH1XfQYaHJEpzvSR4tfdUPqM4fGbLdnFNgDrCPaZziIbkuGltpz/lK/JeN9ew==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0", + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0", "colord": "^2.9.3" } }, "node_modules/@umbraco-ui/uui-color-swatches": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.8.0.tgz", - "integrity": "sha512-qCHYtHPhPsubrVn/cydmigbAde0fc+kJC7ZBxCcuJjyP+wiUhq2/d6dSfYumCcVw1N3Hyj7BRJ/8ZedUIZQ5/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-color-swatches/-/uui-color-swatches-1.10.0.tgz", + "integrity": "sha512-q5vcDckApfL730m59ihiAOwvojMg5t/EHAAqzyKrPO2rqUXB1+SYukWAc7pu12V0Yvqvl6in4zqcyNK8uPdT6g==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-color-swatch": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-color-swatch": "1.10.0" } }, "node_modules/@umbraco-ui/uui-combobox": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.8.0.tgz", - "integrity": "sha512-FVc3PlBYU8S48Zr75pig+5YXh05R3wRKdLl41l7vFBDGGWsgjIsM+vJ6LaM+VoshnTzUTOVvKLE/N0FNTVUvrg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox/-/uui-combobox-1.10.0.tgz", + "integrity": "sha512-Z86/u0PAIUepTL0J7+H1kiJzXFEoHJcaFJQpExFMp6AbCEP11m1Fz17oHipz7uCWQ0DlImrSQcBq+7ed/Y1OLQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-combobox-list": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-popover-container": "1.8.0", - "@umbraco-ui/uui-scroll-container": "1.8.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-combobox-list": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-popover-container": "1.10.0", + "@umbraco-ui/uui-scroll-container": "1.10.0", + "@umbraco-ui/uui-symbol-expand": "1.10.0" } }, "node_modules/@umbraco-ui/uui-combobox-list": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.8.0.tgz", - "integrity": "sha512-3w353u7FdYNLCz6YV+Pk+lxlTeHd2H6rmxzwb+0BHCjbwaw9MLojbhE4JGTCpf/qtk54Xc8Dzg++aznKAYpbVw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-combobox-list/-/uui-combobox-list-1.10.0.tgz", + "integrity": "sha512-5TBt/C6eDyd8TDYVS7oXE0hjDvTqbskjMNBsUTzZwWWGr1G8LtRliEVEc48akSrWSFmNf799OPQhNLWQnJ1UEA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-css": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.8.0.tgz", - "integrity": "sha512-9o9OGUXQK8D9i/VSmH3gUTvH7se+4Ey22dSfbn4Jzcbe6AyGmxKocr/8eZXdkIYwNvK2aUIz/b7GIEbQc4utbA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-css/-/uui-css-1.10.0.tgz", + "integrity": "sha512-bsUBvFGzPzAMegNpznYIzW1CBgxmN2pXfbsgQLpaDJIE8GIW3Y+AW4RNSZV4Tf0uDWIxVlbvI/NruRGLuoCKhA==", "dev": true, + "peer": true, "peerDependencies": { "lit": ">=2.8.0" } }, "node_modules/@umbraco-ui/uui-dialog": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.8.0.tgz", - "integrity": "sha512-QI/Oa7BJ7eEdHcy7hSuFhMPbbPitxnIb2BHVmQzy+YpBShrR3C1GW0FGcYFgJlI/S+jodf3A59VDnVL69gyliQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog/-/uui-dialog-1.10.0.tgz", + "integrity": "sha512-Pkm+YShZbZWHtZ7j27uvabTt33MiAeLL8b/HkTppSCbcVqZ0F/TvUxNBVy0N0mlgvU8c0Zei8b4TPSDfeNnxEQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-css": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-css": "1.10.0" } }, "node_modules/@umbraco-ui/uui-dialog-layout": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.8.0.tgz", - "integrity": "sha512-iv4wEfb6QhCOm8lxg/zH7yrJuVSEEBGgAN67qdvZ9Tu2SFzUCGTzrUcBXlL+U10cbIlq7j6KKvSQvE/IHX40Tg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-dialog-layout/-/uui-dialog-layout-1.10.0.tgz", + "integrity": "sha512-Jdk7FFoyhKqwK8n3+T13CuJUsJ0X/gG0e9XKUC9DdcnJsw5WYx4BhyqPyQpw+7uTZ9GMBSMZ4PMzN30KbMNcvg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-file-dropzone": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.8.0.tgz", - "integrity": "sha512-Fpl/aGprUbcdPngB6xpR8/i7o8HKAWNCUze+N1pXiIVBRXmthEWO6fSm4+LkkkRoZwvYqQSaeu6Mw+Sj+MzHGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-dropzone/-/uui-file-dropzone-1.10.0.tgz", + "integrity": "sha512-2LubyWBGElIVbgyJ+dwxZlAVbO6H3RI0geCQkC52j7KJyR/hZ/G4nFoGw4RFUaL7n7wsWHmyTVQsEalgczdCbg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-symbol-file-dropzone": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-symbol-file-dropzone": "1.10.0" } }, "node_modules/@umbraco-ui/uui-file-preview": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.8.0.tgz", - "integrity": "sha512-WhE/9klwIUjch6PxF+DuFlu+ql0h9YbefMxj/xU4rGUTE/dK4CgA7eVQ/YfAp+WrdtwUFHyp3fThyJvfrodSgA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-file-preview/-/uui-file-preview-1.10.0.tgz", + "integrity": "sha512-1r7BSSF7JHPC3t9YIRCKfchK7jK+43Y9WTHi8lyi2ZpNt3Kju42OQIB9eyFm1+MdJsZi2VkEcBCWTo1wveUasw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-symbol-file": "1.8.0", - "@umbraco-ui/uui-symbol-file-thumbnail": "1.8.0", - "@umbraco-ui/uui-symbol-folder": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-symbol-file": "1.10.0", + "@umbraco-ui/uui-symbol-file-thumbnail": "1.10.0", + "@umbraco-ui/uui-symbol-folder": "1.10.0" } }, "node_modules/@umbraco-ui/uui-form": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.8.0.tgz", - "integrity": "sha512-9NtavsRoh9X39ezzLtwwbK77mUecavcjxP58Jdba/2/6wXRx+vx7qsWV5Rn3OC9XG4tZi6VLFFKahbV8N/jgjw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form/-/uui-form-1.10.0.tgz", + "integrity": "sha512-ByJQV+Lr0iNwth4GXxckoeXtnpRQ1Gnqfo2/Bu53EdEpnpfomrzB6su4AIdaswtHPD+RoM6JVGNtlfzPGtcVvQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-form-layout-item": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.8.1.tgz", - "integrity": "sha512-QHBZayR4T49MAyS9N2jy1rgQ5Yk1JpwoWvWBpbI/WDE718zTjwUJxbLfukq+NnJx7Q1DplZ+IHTnHZkInMC2GQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-layout-item/-/uui-form-layout-item-1.10.0.tgz", + "integrity": "sha512-VdbEhx84mgrWm8puo6RagY9LsaR+kfGp8yPzuzuFXKvWNtKNucT+OmB8OoU6cWJfNahQ89evYeSicIJKakHivw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-form-validation-message": "1.8.1" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-form-validation-message": "1.10.0" } }, "node_modules/@umbraco-ui/uui-form-validation-message": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.8.1.tgz", - "integrity": "sha512-o4WfGHRtV+DrN064DtzyljRkUGYMYEkLHxxbawLowpdmdwyvLByaGOkxfuEJvHgPnncR02//gM04EjulEbGitw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-form-validation-message/-/uui-form-validation-message-1.10.0.tgz", + "integrity": "sha512-TlSvmNAxWmkg5ncKyBrTtMSDvUnGCyn1BxvNfaz8pp4KqGu/sd1a0hBp/80dCa025XH7BJ3d87Kyp9UXLRQi0A==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-icon": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.8.0.tgz", - "integrity": "sha512-hub+KNcwiy+SKxEpI/ei3w1Ficr1SWxcLfwL67MOKS5YyB6RDwSl2FOXx+MkttTAO7PvGBbAgkiiXEkI/rxivg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon/-/uui-icon-1.10.0.tgz", + "integrity": "sha512-nsZyJfcF9MpRXahZ2DS/kzPfJzY3Xql5I/xjjFaS8JEIkT81HzOy1D9bo8AoDrL7VzyaspCbDgLM6R1yhNhlMg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-icon-registry": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.8.0.tgz", - "integrity": "sha512-+55qGgxOBlHmQerxIhKkKJYJgPwnXwsLOBVwF8oYIM1sV58bu7BrGQRUw/K0ytFavrFOb+Easkn62wqzisXsRQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry/-/uui-icon-registry-1.10.0.tgz", + "integrity": "sha512-v/ajwASl4jVSiuGgn4uYetV5NxNghtdZXD7DV0+Lu2u9sV8wIOvk89pWAFsAwDT593/p9/H3p5CoRIzyCmCDIA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0" } }, "node_modules/@umbraco-ui/uui-icon-registry-essential": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.8.0.tgz", - "integrity": "sha512-6+bM30t3+YpknxIjcCHi07eS+MXMkDRP+GBfrUgULqH/EqnJN/OuwMzSdbwFuwzqxmC7thx74Zfl6+JBuIs9lw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-icon-registry-essential/-/uui-icon-registry-essential-1.10.0.tgz", + "integrity": "sha512-0MdN0A4Mz8O1bT22JlHPesvbqMVM+RomtcsHh+DhN3l0RxfamlrSZLEWMavRODq/ign0vfhQ0Zo4iS3fjqzaeg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon-registry": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon-registry": "1.10.0" } }, "node_modules/@umbraco-ui/uui-input": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.8.0.tgz", - "integrity": "sha512-abtQbWWDT+3uo4KVaU+ZbDVKSNtB8r0C/3L7Ql/7xJ2BNI2oSUSAcWoSV6V8Vx0DYh1XWlEQpfSAbk5Fdr7u4w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input/-/uui-input-1.10.0.tgz", + "integrity": "sha512-dnK43VZo7RMOquO8Ih+wurqwlHMmGQ0vdYfc8/DIy3RAeT6+G5ZYJyWmZ3u5jJJ7lBauLJfPVOJAH7BdNPIhmA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-input-file": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.8.0.tgz", - "integrity": "sha512-20fXbIwjyLZWIVsqFt06ldQqA8sHEPm8Y0hmPwbb0nagYfjmIblIE1thT76JA95do7qcU50xGBN9mHt8KvPpQA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-file/-/uui-input-file-1.10.0.tgz", + "integrity": "sha512-GU6cZDEHU5MUQIsQnLAndg2sZ1B4EPPPqI45V2Ynh9ZQUq5OVI+uEyNleiac8yYCM0+w80FE3asJYYETu5inHg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-action-bar": "1.8.0", - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-file-dropzone": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0" + "@umbraco-ui/uui-action-bar": "1.10.0", + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-file-dropzone": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0" } }, "node_modules/@umbraco-ui/uui-input-lock": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.8.0.tgz", - "integrity": "sha512-ZFBssrhCPrCiSfoS6Y9yA9u2dktzNDCRFQ95Z9nQEthhVm2okyqMS3daGz57Vdm4+LVB0HrqIjpEHC6dOqNTBg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-lock/-/uui-input-lock-1.10.0.tgz", + "integrity": "sha512-nIY/lT/sN6R0jSTk27h42CqQcFHEI3JZgzlQz9sb0Z8HZ+uybuh5MWLuElWdDh+3V2On+fKKZUuXVcHERsWfww==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-input": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-input": "1.10.0" } }, "node_modules/@umbraco-ui/uui-input-password": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.8.0.tgz", - "integrity": "sha512-Ra7bR40GzjX11qobHsog2FPGd3FsPzgxPpGLFyMTHzDg2gchYU+KQKkmt6CTzGPgSFuN7DsEW0BMnFnWJ+2moQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-input-password/-/uui-input-password-1.10.0.tgz", + "integrity": "sha512-uffOb2+O/wTk2RgYZmdFZTJwCwhPm56l9/YGkZ+p9O4QBhTIrwEndfdQjGLCo+qj0/skuG0sMzKyWZTbJTRb+Q==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0", - "@umbraco-ui/uui-input": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0", + "@umbraco-ui/uui-input": "1.10.0" } }, "node_modules/@umbraco-ui/uui-keyboard-shortcut": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.8.0.tgz", - "integrity": "sha512-XBO06bZbo7H39k33pm8EoWxm9MP/NXh7W430dLqB5E3q1EOO24PQw2voLsOyegVM3IqgKyMBTO7xHR8NmeZkyg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-keyboard-shortcut/-/uui-keyboard-shortcut-1.10.0.tgz", + "integrity": "sha512-hr3puURGR8DgHOAOa9vzXKFx+WAxfBbQtcg+xw4PHgCDEDV9wLbvnqBOVdP5DrIp3atLCHW355i8T/Fv8ffPqA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-label": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.8.0.tgz", - "integrity": "sha512-Qj3CtfVIv3rVIanTbMvg6UAB2faWGu2mMtvVfmr9kkEP9MGfmA/xgQN94lKlDI7ic54FVkCV4N+1kA5yNkeDSQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-label/-/uui-label-1.10.0.tgz", + "integrity": "sha512-VD8daFTnNgnTgDG8sFmq6JaMMWLDYsTyR5Jl6twrC09GgD2YOn1lFw7mOYpNpKwJv1i5yngXbaT6QCQ+uU1NFg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-loader": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.8.0.tgz", - "integrity": "sha512-CL+n3Icp4ugfn7SBbhbYC4WTBQ+kT27WwJIesOcjw2lmt2l20RQUyBBbZAABx2ayyDuvIzEWnFEzWW8VyVworw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader/-/uui-loader-1.10.0.tgz", + "integrity": "sha512-VDxYhwkojD52zI2PfFAoPI5m83KeU7AILFqDqjySIr5uqjrHv3DlE6BjbfqQHivIvgRKSNlSukukROJZ+bsPiQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-loader-bar": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.8.0.tgz", - "integrity": "sha512-KuUtQ4r/wkbVl4IJVgUb9DCXvV1yvupDw6AAnWuOu7zexuzPfrl32cKBLlnNmhGqnEGcQonX6jf24Lf8T7W6Nw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-bar/-/uui-loader-bar-1.10.0.tgz", + "integrity": "sha512-66BVb/Y2mkb7jHMeQhHGuAuxZ54n2IOeGZ8yVYIs44+U8tXb792Mq6Tr1zgEIzvvmWdfajAjnglhR9hfmijdoQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-loader-circle": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.8.0.tgz", - "integrity": "sha512-L5RyH10Es/stG7CzNzS0bKOzCY+zLSwmqyh0dc8HwzZCvc6XtFHV0KgcxMjmtWWulLsQPgzIOIigf3wefr2VNQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-loader-circle/-/uui-loader-circle-1.10.0.tgz", + "integrity": "sha512-Beg5+Kt3QpPnC31gYMHI2IkVlk8+EU7fzyXSBq+PIaRhhWF8WO0pjsnsXrY0SxrBfSO4qqcGPaB7VDjD/Q4u6g==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-menu-item": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.8.0.tgz", - "integrity": "sha512-0zEMhcw35Evw7cEq1W0GYNVCzIorTVMzFquU7Sg2QiZmk3eiF6HvkF/gHr4d0WR7HvztM1k/eVm+RTs6zp+96g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-menu-item/-/uui-menu-item-1.10.0.tgz", + "integrity": "sha512-DJHBKkp8gWP4x/r0k8NiI8QFQmJscD8iaSKMbRkIBm8cb/Lk7hF/szlo67j1rfoV7iRrGvYhL17p+JaoJ9FyqQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-loader-bar": "1.8.0", - "@umbraco-ui/uui-symbol-expand": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-loader-bar": "1.10.0", + "@umbraco-ui/uui-symbol-expand": "1.10.0" } }, "node_modules/@umbraco-ui/uui-modal": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.8.0.tgz", - "integrity": "sha512-Cqd4pabMLnpnMEa35MKOwXCKQ+5okHYWdvdFRA8x1HqI3AEcz4FNx48nXVH94vhbELv8+fWFAPfrr1v0rvjK6w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-modal/-/uui-modal-1.10.0.tgz", + "integrity": "sha512-Z13sfDV47aDlr7cpsD3YTFpbr1vhFJ/icpSBTKm6oJs4koiG+ZuiVjt//qLqS3eBL8UXgz6rVL5Q+DcWmJOn3Q==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-pagination": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.8.0.tgz", - "integrity": "sha512-SvnWMzbQcTDEN+XQJ0/lVCJ1wL+2L7LA9pfSxJgXIj/pB55Pf3Tt3zMnW8B7RT7i/74atMufaqSSKElZrcPfHQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-pagination/-/uui-pagination-1.10.0.tgz", + "integrity": "sha512-PP0Dsa++77fxv7SqZmRRs8EzKNBQCubPV2t6AEN33fGXjYW83STMJN0BLwZ4za46KbInImAorIbkWA3kEetZQQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-button-group": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-button-group": "1.10.0" } }, "node_modules/@umbraco-ui/uui-popover": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.8.0.tgz", - "integrity": "sha512-bHERyYItLAvtWbpOdPgmuPFgYs2TuKImm3h04DtQRatYP4dq8wg0tftVyZK3k9TVfLMvStOy2EyzybTD+1ZmBg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover/-/uui-popover-1.10.0.tgz", + "integrity": "sha512-gPEVFVeFqa76bGwzkxc8Gjt5EbLKd2WgUeFGIBrP8+ZscbDyP/eG6bXjfUSBweXedPFxmlN/Ng3Dtz+KU2QIGQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-popover-container": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.8.0.tgz", - "integrity": "sha512-FHaX4sIa7q4HTVzMr9w3Z6zuunPuDROnHjVS6QkovqHLEgQjeTQB8hGAxN6cd3QsFbgbtC9fzBo8zTn9yxJLmA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-popover-container/-/uui-popover-container-1.10.0.tgz", + "integrity": "sha512-YguOGWH7XfD7tsopKoh5S6UgymufLna/1xxsBt88/FdF/m1xzHHmrgqWy/GnNycXgGgSulJ3fYe1OG2JhIOHRQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-progress-bar": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.8.0.tgz", - "integrity": "sha512-WX+YjiH0HCljtzTImR6Q8bf06wm+jcCgEOE10pMRri0r4nyBCAN7Zms23sHj6rR+S/oxoYcuREqdWXWP4eRfFA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-progress-bar/-/uui-progress-bar-1.10.0.tgz", + "integrity": "sha512-ymOWS5R8l3xU3NOPCXAb2gsRvHVjgu6zCGyUreDj0Lz1glagkPKM2evttui9ixoVV8CR7SxAj3hK6t+PXp2ubA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-radio": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.8.0.tgz", - "integrity": "sha512-3kPWyc0ZbtAIeOHxAKudSslomg1zKy4RMgrfDeNumQhuCei0VWhVn76Xyfu/Gl2n4XnLIzZY3zv4XCaxwWvJZw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-radio/-/uui-radio-1.10.0.tgz", + "integrity": "sha512-ZvC7wgVB4Odn7n8oYsyXOLeIGhx6Ej/Np67Eqg3l+YSxee+SZzBKIHnqSo6BrgaNDSuSS+gWXS8vREGss1UVjg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-range-slider": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.8.0.tgz", - "integrity": "sha512-ThDfLvxRMfP93HQobgLaglR860vQiwCM63PXp2igXWZl69ikDDa7HuraupEpmdL13VLKAv5L1BMue1ItC1x2MA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-range-slider/-/uui-range-slider-1.10.0.tgz", + "integrity": "sha512-suYF7UnErKmWyjnPdua6GwmzUe+FovKMyePKISk17gicoiCfked1ygQi0w7YFPJeo7hScx1MA9sUpvh6TVDrkw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.8.0.tgz", - "integrity": "sha512-mcw7faAXeG/2QfLeuYsB950seU0FQH5Pd+aSfi6zwgWkCasxKWLrlslVTP1U5zD5WFkNt4ls6RnMGZhN6bq7mQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref/-/uui-ref-1.10.0.tgz", + "integrity": "sha512-dDbpHYQeMQGF/3pudcj0B6y3ATN8/IQqF9fWlbz/L4H9oAxmiiFgujwXpzLqLx2j9IQvw+pbOx2fi7rVpKsMfw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-list": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.8.0.tgz", - "integrity": "sha512-9iiTYVyw+dpO1gEB2oMJSe2DOVvA3a2uY5FLwkRgpAjvzbhD9hhyGLcVgtvM1rxUYc9SvJbGJXk2tPY4Nit3pA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-list/-/uui-ref-list-1.10.0.tgz", + "integrity": "sha512-XvDCMM4WTEADKqV/QIqy9gzFe1M6w6XQjX5dVE64Luc2lEjvzKf+/LKcJaoULJsd72roh31MbXNvCO6AdGTqdA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.8.0.tgz", - "integrity": "sha512-KOGy10QhPUQFNFNPkmqro1YqFg5Y5b2N/DwkcYP/xyu4Kc1f5FuE+Uo5L41i2KdbDP6O+Tz5gsU6HpPvpEAc7Q==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node/-/uui-ref-node-1.10.0.tgz", + "integrity": "sha512-gRu81TImEcJdJ1PKs7glmFXWwP4NkkuvEg0EDXEZoS1ORK7Ms/rLgjecnjruTU2oqxlEiTeSpy3fvw8Ybc+Wyg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-ref": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-ref": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-data-type": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.8.0.tgz", - "integrity": "sha512-x7PicpHE7PPZrVDojKO5ornG7HZoRB9/pjCZWV+8wxPznyGmFvcJpbRQLdIVvvXVkL1a0c4uLY2CsUO+K52Rbg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-data-type/-/uui-ref-node-data-type-1.10.0.tgz", + "integrity": "sha512-4PxcAdF8wgKfBepHL5xmUKB6i1i5lbHKJUOPmo67N/Vj/xPeBfc899mfv9zFxHV2i5q7FGrxexDEkjtvp2QkYg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-document-type": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.8.0.tgz", - "integrity": "sha512-QjeC8DvgA/WFb3wOUXMOALK5SoKeDAvYqNp1wNvU5AMxJY+5CTfi6JNWKZsI4K+Mh3lfzPP+HaWx5MTwt5epNQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-document-type/-/uui-ref-node-document-type-1.10.0.tgz", + "integrity": "sha512-sKqOGeqbLoJfOrstQebwNv/Mu+Zn5MJOUEyhKgYU04Xh5alpQuEm8G1fmrWYlR3RVeN0APGhl8zC0GJToThw4g==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-form": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.8.0.tgz", - "integrity": "sha512-sm0xSru6pv8yK9qepzF5Wzzd1jarMBZFtzgqw5H65pGTP6pNhNG4GIkeXLfd2TH9g3e6biJNKAOFJ5w8Tz4O9A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-form/-/uui-ref-node-form-1.10.0.tgz", + "integrity": "sha512-c7fJdsNswUnbCl92zWSU6cxKoimPKqNstoHGcDXfy0GTW0pPQqdL/Ux2ymuY84U1HfJxMsc+hC21KVmG/N0oxw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-member": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.8.0.tgz", - "integrity": "sha512-z9l44zCER4KmNMSnCy6BFxXZ6awr/L405pJJJzqb3GAXsBip47pCzhYgxCbekB+xSY+E0hS1EuG1JJ5wn05yiQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-member/-/uui-ref-node-member-1.10.0.tgz", + "integrity": "sha512-ZrSb8b6/hizqqlKtcaCxg0A/L8hBblxiXpMuxx+vD0ihYLJt6fYBFo6NI2KGqAztTd/5/Bih+7Ayy33gh7+0Eg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-package": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.8.0.tgz", - "integrity": "sha512-bpWVBOm39tl5jY3Y+qn5dnikmnaAOrHEg2SBf/11d7aHOauEgDiJZQmM9BzjAeionaZrj4beYJw5szazaVkpDQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-package/-/uui-ref-node-package-1.10.0.tgz", + "integrity": "sha512-yaHAx0NOWIBDs+eHqvKMIgqHvTIRvPEj0O9c8smTDPaXNiIpTdzikRoqbFfp9QoPipK2Yzgtdzx6FxwnkOldJw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-ref-node-user": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.8.0.tgz", - "integrity": "sha512-Z91Me56phGFyt/hJaRDnXrorlYVjrL6WaUWRDZAi+mqqThFriG1DVeaFEewZ1yeD3Hy6haHanDa7dtwm2FLsBQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-ref-node-user/-/uui-ref-node-user-1.10.0.tgz", + "integrity": "sha512-+gFVF/gY3VU6NqwI1Ns54Ly7LNEIlTh891MTLxum/3WidyhCQfHEjlFpjEtyBYafWY2/dS54/9ST7wg8+wLFlQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-ref-node": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-ref-node": "1.10.0" } }, "node_modules/@umbraco-ui/uui-scroll-container": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.8.0.tgz", - "integrity": "sha512-PHxOZhsM53J3SQaXhH0euUwshr6Ka06iFEmtNKaqVe+nPwTjYHWHDuulvp7/qm/btjzSIrJqKgs02ft8wGqXwA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-scroll-container/-/uui-scroll-container-1.10.0.tgz", + "integrity": "sha512-BX/ECh7lsJPbNzQD6N43bMyNTk4EROG6L9LbQja/YUYB6/9CH/uaOpve31vFyykjQTi84QFf/C4zWcuLAfQHPg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-select": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.8.0.tgz", - "integrity": "sha512-xFXo7IlAtofZCWIqnhyThIjP8ORRwo6786fv5yarayhqlKeAwJRD5t6QtX4z5z/1b/zW11oF2GkJzrzp4KN4vw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-select/-/uui-select-1.10.0.tgz", + "integrity": "sha512-J2Oif7zwWaGvmV+04B6oAi37+AWsId9sfBy9LHswuovoe4wOf2mwIiSXRfJZ7hODfoS9g8y9Y/usX09CZhPZVA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-slider": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.8.0.tgz", - "integrity": "sha512-qKgWyVzMKF8RVwwgpdMvKfCS3TEyMbZj/ZKClgTJJfs+3r8Q002F7irx7Lgh+mDww+jLsuMtG/cu0xSXU4HC4w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-slider/-/uui-slider-1.10.0.tgz", + "integrity": "sha512-opDTKBsfzzOlQzTCx+HYGnYWHGgPYtyFCHdHzsfJAl9o010mIMLmiujqD/VLifKYfKETXFJuRjWMhpSIAn3msQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-expand": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.8.0.tgz", - "integrity": "sha512-AfpoR4dTOL4gEfP9lnEVymU3mlNfjFSuk8TGbqy0jVMTMbYeol5Bcl6jJFqqPd1npfgT7FPZH9zrMkcFogfSSw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-expand/-/uui-symbol-expand-1.10.0.tgz", + "integrity": "sha512-hmZrRLaGGaNM65vuUI6bfAIHMfN59Ba3bpiHcEXUvtS1lMsyydGgfZlVuzW7ZlUUEdRj3FRhdwyATuTVUDkhCg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-file": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.8.0.tgz", - "integrity": "sha512-/2O0TNl+Sx/cCy2kQlSCOujvRwz+Rxg9JxyMX5Vc14ZqrVJ4FsD2S/jJWLtE2YJ+EtLoc15Zzw2GogZO7aBcLQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file/-/uui-symbol-file-1.10.0.tgz", + "integrity": "sha512-DitXeZrr2X5bRNqP8Etxdg/0oN0PKwxOVdOndAUxvW/5nSQMEPu6YBR4VBPwvTBrO/O3aXW7fe99yMnQ0mILKg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-dropzone": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.8.0.tgz", - "integrity": "sha512-n2QRKTEnvQQgiyTQ7uVbz7XsGL0HRwaEtLqEMbaON6oYCsGWFFsbp8QqyHdB8iBQSrlV9I1J4sS0e5Ry+W25YQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-dropzone/-/uui-symbol-file-dropzone-1.10.0.tgz", + "integrity": "sha512-jTF+20vxDQzhpcuqEFbub+5EkCgEZb7OVYBhgxCUW9SftoB5EWaGYR+9lpz5FNjqBQJi5FTR08oji8gFEbmiEA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-file-thumbnail": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.8.0.tgz", - "integrity": "sha512-KdFOrfVIwtjavoa+S5ro1gi2Er8IPqXnY6gpTRpAgfO/f+/ZRg6AwPKn4SCc7QqJ8ThHpsg8wki8WGiK04rfbA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-file-thumbnail/-/uui-symbol-file-thumbnail-1.10.0.tgz", + "integrity": "sha512-ZbXqXD8MyrHPMTgqwSy81mjaSgb8ZYgkZ6a7M2WNWqL5cpzQ7URUUuT/3U+VDreMexyl9Yy60soWbr2zrjBuqQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-folder": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.8.0.tgz", - "integrity": "sha512-g7FIonq/5wHH2+e/+DhB+t3E4wu7dM4MrKxLsP6b8JmYz7Y0t9OlTBT5J+i3P8YnJKYY6i5V5Eip4QNWJqC+sQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-folder/-/uui-symbol-folder-1.10.0.tgz", + "integrity": "sha512-7gCGuIl8WPwZKVjR5+Tcb5CjAFL7i9kdbpKdDXGpComyZUpfIzy+2Eeb6H0N1P7M6c9gWJkvl06hghI7XJpz+A==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-lock": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.8.0.tgz", - "integrity": "sha512-+/K59hTkBJr6bYOrTgPtvZEVsr59MPNwvIHhGm685WZPZrNA9dGPZrO9vnw1eyNq70XYuHkiNkmKUmna9mQmIA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-lock/-/uui-symbol-lock-1.10.0.tgz", + "integrity": "sha512-8kziG7cQxd0Xjo3XdBBorZ7AwFw0joI7xJCTsFbymIvwRYp5hiJbrj2Kmf4kxs2rCcXPaWjI9D9nr/41EJqO2A==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-more": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.8.0.tgz", - "integrity": "sha512-BSWQ05XYJSS6WKpA6//QnSnMehV5py5j8bxl7bZzmrthr2SwyejwS+pGYq7rTqWw7BBk1mA8I7Zkl+kVph/7+g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-more/-/uui-symbol-more-1.10.0.tgz", + "integrity": "sha512-Hp2/BP8JDatgI/WVxfriZ/5IhlzvxFOyBzKNi/EIGPQYvMRofdp0GAI1UhT5MmWe6J5R/q9v106CWaDSNUP9pw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-symbol-sort": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.8.0.tgz", - "integrity": "sha512-+IFJYlPsUvJYNDc8sWB4zan/dTCCj4vkqwXALW3xLSxpsKSvtSvXPzXK/i4YwaT4Azx4hfrWIW2cz6/h5JDonA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-symbol-sort/-/uui-symbol-sort-1.10.0.tgz", + "integrity": "sha512-J6EyHkY0hT7ZcZrh3JhCXlKnHE6xC9CXzIZ3EJ0lIPZOBLOql2okut0g/ZdP5s4JM7zGOEUEwEFwO+duxxacYA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-table": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.8.0.tgz", - "integrity": "sha512-nbRoValRn17SadfpGKKT1RfyoRlCLhvij2BRMw1KyldztWlWGozPQSDjqhcEPSzJZCrNV76nIMbg5FLfsTl4iA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-table/-/uui-table-1.10.0.tgz", + "integrity": "sha512-AoqRaRAfI/WokEuDtE1utl5HVh05l/4+gpUWUj1vzyTNoVeBH3pMxg93ZDlus5pntNavP4foYl4GyTlPSVXcXg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-tabs": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.8.0.tgz", - "integrity": "sha512-xnZvjjmRHJbyC9bd6LMYfHA8Jjb51GTJuFAd4j4S9NVZYomQDBFl7IKVWtUFzQvVzk93zJHVSWO8vmtNLQZreg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tabs/-/uui-tabs-1.10.0.tgz", + "integrity": "sha512-97kkyWEyTvbNVFvcsD4Q9Av2SSwlRN+bdTZe+v1s4gROLJTef9UXs53N68WcjjPZvjBuVL0MpcxZ6kYTs9oxOg==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-popover-container": "1.8.0", - "@umbraco-ui/uui-symbol-more": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-popover-container": "1.10.0", + "@umbraco-ui/uui-symbol-more": "1.10.0" } }, "node_modules/@umbraco-ui/uui-tag": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.8.0.tgz", - "integrity": "sha512-5Dt7EsvACfs75bsncIDvqitXYub2Rfntbrc3gzXLHjqOQy2YBL5s/HNGz3xsf5msKuDAR0dmTyxhItaDAi7EkQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-tag/-/uui-tag-1.10.0.tgz", + "integrity": "sha512-CtR6XcvM9DXBhZrVmngeT2aMsx5D38DnJCDLZlxcNyqbfL7U6FH8QGTWO9Htepln/hPr48VyTMV0yHs/mKfpHQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-textarea": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.8.0.tgz", - "integrity": "sha512-WYWiD3x1DXbsAALmTT2txoeeqiZ9J/FlkTGL1Wasu89jfm8bAgxUG5wuoa8SL4r79rAF+RUDrJPygeUqDm0N8A==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-textarea/-/uui-textarea-1.10.0.tgz", + "integrity": "sha512-hZKAhzDYqGVGqR2aZ+TsE/YygfKXOUs8i0OGeecUEuiEsL18+Js5Y2qyeb8pq5GUE2Mu6nJx2FkH8KThRCoLug==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/@umbraco-ui/uui-toast-notification": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.8.0.tgz", - "integrity": "sha512-62q36/jggXp+GdRSzseQ7d63hUIXtHIXe/5bnSSHXcxxIcnbf9Sy3pkBkOYM7CXgBUUwt9T9IHLZ6eGw/6K9Xw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification/-/uui-toast-notification-1.10.0.tgz", + "integrity": "sha512-dVPSRVPDblNDeqKMEVZx2PePyn/qfKtq6pu5k6gqh5aQhYZt2GyyV/oHELgf+VYNzzfgdN65w2cd78i3Ug5fVw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-button": "1.8.0", - "@umbraco-ui/uui-css": "1.8.0", - "@umbraco-ui/uui-icon": "1.8.0", - "@umbraco-ui/uui-icon-registry-essential": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-button": "1.10.0", + "@umbraco-ui/uui-css": "1.10.0", + "@umbraco-ui/uui-icon": "1.10.0", + "@umbraco-ui/uui-icon-registry-essential": "1.10.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-container": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.8.0.tgz", - "integrity": "sha512-NdILHgGvKFF+JZGejTJnXt1LdJIl/MxmPUj6+rvEGCO2SDhZmIOHjnIohT8HFytxmJqyWJpryPQjo6KJezRVbQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-container/-/uui-toast-notification-container-1.10.0.tgz", + "integrity": "sha512-s+Uxe+IDLvzg+cj1+icFzVYU+8UB/XgFZetLOI7PLe5edbvFVwld0UmminQ9n1KYbyxGOnxvvhCgBfrgpiFkLw==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-toast-notification": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-toast-notification": "1.10.0" } }, "node_modules/@umbraco-ui/uui-toast-notification-layout": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.8.0.tgz", - "integrity": "sha512-NyGFv3kAcU8XMxLAyDhy3dt1oIHOwbAYO5+Utm4CFAAvcJzVTNn948Sp0dTdcfSjXjZG+3Ufv/Qb/OpcrybJ/w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toast-notification-layout/-/uui-toast-notification-layout-1.10.0.tgz", + "integrity": "sha512-TY6DUEDqXXvNpe7O/j1fanBeWxyeV6Mc9jpXY2ERXrDCaPKL1uEUl2ouIrOCBw5OQMJsoZBU8ZtZmkGRRjlu4w==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-css": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-css": "1.10.0" } }, "node_modules/@umbraco-ui/uui-toggle": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.8.0.tgz", - "integrity": "sha512-PNk2qeaL7bJXnSkG0T1J5pheNy7yTeaVbdyXuL/9fkoIdb9IkD/h6XLE9niiyWYF1xrdjpgAKZ32u0Oc4qXVyA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-toggle/-/uui-toggle-1.10.0.tgz", + "integrity": "sha512-uSBf7j1f66rKHnfYXzJdkEDAx0WLukMpQ8zD7ZhvsZ6fEfNP31JbWDaWM7quHshXhk05/wVOgU7fa+6D7sCleA==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0", - "@umbraco-ui/uui-boolean-input": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0", + "@umbraco-ui/uui-boolean-input": "1.10.0" } }, "node_modules/@umbraco-ui/uui-visually-hidden": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.8.0.tgz", - "integrity": "sha512-3a4/B2uM3YfXjI9dyfSL2Z47ziwW7HuYoozNderKO/I7l0CgxgoHIOwF1sRb3QgOlsFhhfeKdndvU7SbD6tazQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@umbraco-ui/uui-visually-hidden/-/uui-visually-hidden-1.10.0.tgz", + "integrity": "sha512-Jp+tg8v2Ujth+HSP8W/JZth6QaeqWO2qbLhCCifEwvU4M7/ehmavcm+JnFx8zICkHrSsyL+p7yH4iXJ3H4eGOQ==", "dev": true, "peer": true, "dependencies": { - "@umbraco-ui/uui-base": "1.8.0" + "@umbraco-ui/uui-base": "1.10.0" } }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -1826,9 +1276,8 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1838,18 +1287,16 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1862,8 +1309,6 @@ }, "node_modules/base64-js": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -1879,13 +1324,13 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "peer": true }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1899,9 +1344,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -1911,18 +1355,16 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1934,9 +1376,8 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1951,9 +1392,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1963,9 +1403,8 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/colord": { "version": "2.9.3", @@ -1976,18 +1415,16 @@ }, "node_modules/cookie": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/debug": { "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -2002,40 +1439,36 @@ }, "node_modules/diff": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.3.1" } }, "node_modules/dompurify": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz", - "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", "dev": true, "peer": true }, "node_modules/element-internals-polyfill": { "version": "1.3.11", - "resolved": "https://registry.npmjs.org/element-internals-polyfill/-/element-internals-polyfill-1.3.11.tgz", - "integrity": "sha512-SQLQNVY4wMdpnP/F/HtalJbpEenQd46Avtjm5hvUdeTs3QU0zHFNX5/AmtQIPPcfzePb0ipCkQGY4GwYJIhLJA==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2070,19 +1503,16 @@ }, "node_modules/escalade": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -2093,86 +1523,79 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/globrex": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/graphql": { "version": "16.9.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", - "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/headers-polyfill": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-node-process": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lit": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.4.tgz", - "integrity": "sha512-q6qKnKXHy2g1kjBaNfcoLlgbI3+aSOZ9Q4tiGa9bGYXq5RBXxkVTqTIVmP2VWMp29L4GyvCFm8ZQ2o56eUAMyA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.0.tgz", + "integrity": "sha512-s6tI33Lf6VpDu7u4YqsSX78D28bYQulM+VAzsGch4fx2H0eLZnJsUBsPWmGYSGoKDNbjtRv02rio1o+UdPVwvw==", "dev": true, "peer": true, "dependencies": { "@lit/reactive-element": "^2.0.4", - "lit-element": "^4.0.4", - "lit-html": "^3.1.2" + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" } }, "node_modules/lit-element": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.6.tgz", - "integrity": "sha512-U4sdJ3CSQip7sLGZ/uJskO5hGiqtlpxndsLr6mt3IQIjheg93UKYeGQjWMRql1s/cXNOaRrCzC2FQwjIwSUqkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.0.tgz", + "integrity": "sha512-gSejRUQJuMQjV2Z59KAS/D4iElUhwKpIyJvZ9w+DIagIQjfJnhR20h2Q5ddpzXGS+fF0tMZ/xEYGMnKmaI/iww==", "dev": true, "peer": true, "dependencies": { "@lit-labs/ssr-dom-shim": "^1.2.0", "@lit/reactive-element": "^2.0.4", - "lit-html": "^3.1.2" + "lit-html": "^3.2.0" } }, "node_modules/lit-html": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.4.tgz", - "integrity": "sha512-yKKO2uVv7zYFHlWMfZmqc+4hkmSbFp8jgjdZY9vvR9jr4J8fH6FUMXhr+ljfELgmjpvlF7Z1SJ5n5/Jeqtc9YA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.0.tgz", + "integrity": "sha512-pwT/HwoxqI9FggTrYVarkBKFN9MlTUpLrDHubTmW4SrkL3kkqW5gxwbxMMUnbbRHBC0WTZnYHcjDSCM559VyfA==", "dev": true, "peer": true, "dependencies": { @@ -2180,9 +1603,9 @@ } }, "node_modules/marked": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", - "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", "dev": true, "peer": true, "bin": { @@ -2193,24 +1616,22 @@ } }, "node_modules/monaco-editor": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.48.0.tgz", - "integrity": "sha512-goSDElNqFfw7iDHMg8WDATkfcyeLTNpBHQpO8incK6p5qZt5G/1j41X0xdGzpIkGojGXM+QiRQyLjnfDVvrpwA==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.50.0.tgz", + "integrity": "sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==", "dev": true, "peer": true }, "node_modules/ms": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/msw": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.1.tgz", - "integrity": "sha512-ocgvBCLn/5l3jpl1lssIb3cniuACJLoOfZu01e3n5dbJrpA5PeeWn28jCLgQDNt6d7QT8tF2fYRzm9JoEHtiig==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", @@ -2250,17 +1671,14 @@ }, "node_modules/mute-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/nanoid": { "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -2268,6 +1686,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -2277,26 +1696,21 @@ }, "node_modules/outvariant": { "version": "1.4.2", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", - "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/picocolors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/postcss": { "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "dev": true, "funding": [ { @@ -2312,6 +1726,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -2323,18 +1738,16 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/rollup": { "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -2367,9 +1780,8 @@ }, "node_modules/rxjs": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "peer": true, "dependencies": { "tslib": "^2.1.0" @@ -2377,9 +1789,8 @@ }, "node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2389,33 +1800,29 @@ }, "node_modules/source-map-js": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/strict-event-emitter": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2427,9 +1834,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2439,9 +1845,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2451,23 +1856,21 @@ }, "node_modules/tinymce": { "version": "6.8.4", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.4.tgz", - "integrity": "sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg==", "dev": true, + "license": "MIT", "peer": true }, "node_modules/tinymce-i18n": { - "version": "24.6.24", - "resolved": "https://registry.npmjs.org/tinymce-i18n/-/tinymce-i18n-24.6.24.tgz", - "integrity": "sha512-FToQhgKzZLqEg+twKVjUcS8gPJbZprOtiyGhGHhxGZMeqJITgbD0imc2QV7cZ82cZbHTBOkK2aOvgmbhk5uaTw==", + "version": "24.9.23", + "resolved": "https://registry.npmjs.org/tinymce-i18n/-/tinymce-i18n-24.9.23.tgz", + "integrity": "sha512-oK59vT2LzdJ3roHWwbAmMJLeDdWJCtXCOakNsG5DrEsQJq+MMa/fbn5lBAAql8mwaYOKUCWCrW1b87rtE6KHoA==", "dev": true, "peer": true }, "node_modules/tsconfck": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.0.tgz", - "integrity": "sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==", "dev": true, + "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -2485,16 +1888,14 @@ }, "node_modules/tslib": { "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true, + "license": "0BSD", "peer": true }, "node_modules/type-fest": { "version": "4.20.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz", - "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -2504,9 +1905,8 @@ }, "node_modules/typescript": { "version": "5.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", - "integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2517,14 +1917,13 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", @@ -2537,9 +1936,8 @@ }, "node_modules/vite": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.38", @@ -2592,9 +1990,8 @@ }, "node_modules/vite-tsconfig-paths": { "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -2611,9 +2008,8 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2625,18 +2021,16 @@ }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2652,9 +2046,8 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } diff --git a/src/Umbraco.Web.UI.Login/package.json b/src/Umbraco.Web.UI.Login/package.json index a55731fb70..d9c4628df1 100644 --- a/src/Umbraco.Web.UI.Login/package.json +++ b/src/Umbraco.Web.UI.Login/package.json @@ -15,8 +15,7 @@ "dependencies": { }, "devDependencies": { - "@umbraco-cms/backoffice": "^14.0.0", - "@umbraco-ui/uui-css": "^1.10.0", + "@umbraco-cms/backoffice": "^14.2.0", "msw": "^2.3.0", "typescript": "^5.4.5", "vite": "^5.2.11", From c6e3b917738f09c9940e4ba9e8ce77e64aca5c93 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 24 Sep 2024 10:42:47 +0200 Subject: [PATCH 27/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 47359b5de8..3ce4c5f3cb 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 47359b5de85db162c3210783e9bbe002e1e4aec3 +Subproject commit 3ce4c5f3cbdef6f9917711a7e49656f3a560966a From 1b4cb87370e2dce29f69898e689f01c136df7e74 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 24 Sep 2024 11:02:22 +0200 Subject: [PATCH 28/38] Fixed test build --- .../DocumentHybridCacheMockTests.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs index 5fc467f2f6..d6402b2603 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/DocumentHybridCacheMockTests.cs @@ -103,7 +103,8 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent GetRequiredService(), GetRequiredService(), GetSeedProviders(), - Options.Create(new CacheSettings())); + Options.Create(new CacheSettings()), + GetRequiredService()); _mockedCache = new DocumentCache(_mockDocumentCacheService, GetRequiredService()); } @@ -163,7 +164,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent await _mockDocumentCacheService.DeleteItemAsync(Textpage); _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; - await _mockDocumentCacheService.SeedAsync(); + await _mockDocumentCacheService.SeedAsync(CancellationToken.None); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id); AssertTextPage(textPage); @@ -184,7 +185,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent await _mockDocumentCacheService.DeleteItemAsync(Textpage); _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; - await _mockDocumentCacheService.SeedAsync(); + await _mockDocumentCacheService.SeedAsync(CancellationToken.None); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key); AssertTextPage(textPage); @@ -198,7 +199,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent await _mockDocumentCacheService.DeleteItemAsync(Textpage); _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; - await _mockDocumentCacheService.SeedAsync(); + await _mockDocumentCacheService.SeedAsync(CancellationToken.None); var textPage = await _mockedCache.GetByIdAsync(Textpage.Id, true); AssertTextPage(textPage); @@ -211,7 +212,7 @@ public class DocumentHybridCacheMockTests : UmbracoIntegrationTestWithContent _cacheSettings.ContentTypeKeys = [ Textpage.ContentType.Key ]; await _mockDocumentCacheService.DeleteItemAsync(Textpage); - await _mockDocumentCacheService.SeedAsync(); + await _mockDocumentCacheService.SeedAsync(CancellationToken.None); var textPage = await _mockedCache.GetByIdAsync(Textpage.Key, true); AssertTextPage(textPage); From b93fe6c632bfb46fb90db53e4064d889a7d6b3d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 24 Sep 2024 11:08:38 +0200 Subject: [PATCH 29/38] Fix: update login package lock (#17116) * update package lock * update backoffice submodule --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- src/Umbraco.Web.UI.Client | 2 +- src/Umbraco.Web.UI.Login/package-lock.json | 1063 ++++++++++++++--- .../public/mockServiceWorker.js | 2 +- 3 files changed, 890 insertions(+), 177 deletions(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 3ce4c5f3cb..4729d3baa7 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 3ce4c5f3cbdef6f9917711a7e49656f3a560966a +Subproject commit 4729d3baa7611ed63380abcfc184c1bb5a48b3bb diff --git a/src/Umbraco.Web.UI.Login/package-lock.json b/src/Umbraco.Web.UI.Login/package-lock.json index 054a6c5be0..64c40cebe4 100644 --- a/src/Umbraco.Web.UI.Login/package-lock.json +++ b/src/Umbraco.Web.UI.Login/package-lock.json @@ -19,27 +19,104 @@ }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", "dev": true, - "license": "ISC", "dependencies": { "cookie": "^0.5.0" } }, "node_modules/@bundled-es-modules/statuses": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", "dev": true, - "license": "ISC", "dependencies": { "statuses": "^2.0.1" } }, - "node_modules/@esbuild/darwin-arm64": { + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -48,53 +125,359 @@ "node": ">=12" } }, - "node_modules/@inquirer/confirm": { - "version": "3.1.10", + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", + "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", "dev": true, - "license": "MIT", "dependencies": { - "@inquirer/core": "^8.2.3", - "@inquirer/type": "^1.3.3" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/core": { - "version": "8.2.3", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", "dev": true, - "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.3", - "@inquirer/type": "^1.3.3", + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", "@types/mute-stream": "^0.0.4", - "@types/node": "^20.14.6", + "@types/node": "^22.5.5", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "cli-spinners": "^2.9.2", "cli-width": "^4.1.0", "mute-stream": "^1.0.0", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/figures": { - "version": "1.0.3", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.6.tgz", + "integrity": "sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@inquirer/type": { - "version": "1.3.3", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", "dev": true, - "license": "MIT", + "dependencies": { + "mute-stream": "^1.0.0" + }, "engines": { "node": ">=18" } @@ -116,24 +499,17 @@ "@lit-labs/ssr-dom-shim": "^1.2.0" } }, - "node_modules/@mswjs/cookies": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@mswjs/interceptors": { - "version": "0.29.1", + "version": "0.35.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.8.tgz", + "integrity": "sha512-PFfqpHplKa7KMdoQdj5td03uG05VK2Ng1dG0sP4pT9h0dGSX2v9txYt/AnrzPb/vAmfyBBC0NQV7VaBEX+efgQ==", "dev": true, - "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", - "outvariant": "^1.2.1", + "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" }, "engines": { @@ -142,13 +518,15 @@ }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true }, "node_modules/@open-draft/logger": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", "dev": true, - "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" @@ -156,36 +534,236 @@ }, "node_modules/@open-draft/until": { "version": "2.1.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@types/cookie": { "version": "0.6.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true }, "node_modules/@types/diff": { - "version": "5.2.1", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-qVqLpd49rmJA2nZzLVsmfS/aiiBpfVE95dHhPVwG0NmSBAt+riPxnj53wq2oBq5m4Q2RF1IWFEUpnZTgrQZfEQ==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/@types/dompurify": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", "dev": true, - "license": "MIT", "peer": true, "dependencies": { "@types/trusted-types": "*" @@ -193,34 +771,45 @@ }, "node_modules/@types/estree": { "version": "1.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/mute-stream": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.14.9", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", "dev": true, - "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/statuses": { "version": "2.0.5", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true }, "node_modules/@types/trusted-types": { "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/@types/uuid": { @@ -232,8 +821,9 @@ }, "node_modules/@types/wrap-ansi": { "version": "3.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true }, "node_modules/@umbraco-cms/backoffice": { "version": "14.2.0", @@ -1262,8 +1852,9 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -1276,8 +1867,9 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1287,16 +1879,18 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1309,6 +1903,8 @@ }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, "funding": [ { @@ -1324,13 +1920,13 @@ "url": "https://feross.org/support" } ], - "license": "MIT", "peer": true }, "node_modules/chalk": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1342,29 +1938,20 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-width": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, - "license": "ISC", "engines": { "node": ">= 12" } }, "node_modules/cliui": { "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1376,8 +1963,9 @@ }, "node_modules/cliui/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1392,8 +1980,9 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1403,8 +1992,9 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/colord": { "version": "2.9.3", @@ -1415,18 +2005,20 @@ }, "node_modules/cookie": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/debug": { - "version": "4.3.5", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, - "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1439,8 +2031,9 @@ }, "node_modules/diff": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, - "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.3.1" @@ -1455,20 +2048,23 @@ }, "node_modules/element-internals-polyfill": { "version": "1.3.11", + "resolved": "https://registry.npmjs.org/element-internals-polyfill/-/element-internals-polyfill-1.3.11.tgz", + "integrity": "sha512-SQLQNVY4wMdpnP/F/HtalJbpEenQd46Avtjm5hvUdeTs3QU0zHFNX5/AmtQIPPcfzePb0ipCkQGY4GwYJIhLJA==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/esbuild": { "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -1502,17 +2098,20 @@ } }, "node_modules/escalade": { - "version": "3.1.2", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/fsevents": { "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "license": "MIT", + "hasInstallScript": true, "optional": true, "os": [ "darwin" @@ -1523,50 +2122,57 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/globrex": { "version": "0.1.2", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true }, "node_modules/graphql": { "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/headers-polyfill": { "version": "4.0.3", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-node-process": { "version": "1.2.0", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true }, "node_modules/lit": { "version": "3.2.0", @@ -1623,21 +2229,23 @@ "peer": true }, "node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/msw": { - "version": "2.3.1", + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.9.tgz", + "integrity": "sha512-1m8xccT6ipN4PTqLinPwmzhxQREuxaEJYdx4nIbggxP8aM7r1e71vE7RtOUSQoAm1LydjGfZKy7370XD/tsuYg==", "dev": true, "hasInstallScript": true, - "license": "MIT", "dependencies": { "@bundled-es-modules/cookie": "^2.0.0", "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", "@inquirer/confirm": "^3.0.0", - "@mswjs/cookies": "^1.1.0", - "@mswjs/interceptors": "^0.29.0", + "@mswjs/interceptors": "^0.35.8", "@open-draft/until": "^2.1.0", "@types/cookie": "^0.6.0", "@types/statuses": "^2.0.4", @@ -1646,7 +2254,7 @@ "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.2", - "path-to-regexp": "^6.2.0", + "path-to-regexp": "^6.3.0", "strict-event-emitter": "^0.5.1", "type-fest": "^4.9.0", "yargs": "^17.7.2" @@ -1661,7 +2269,7 @@ "url": "https://github.com/sponsors/mswjs" }, "peerDependencies": { - "typescript": ">= 4.7.x" + "typescript": ">= 4.8.x" }, "peerDependenciesMeta": { "typescript": { @@ -1671,14 +2279,17 @@ }, "node_modules/mute-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/nanoid": { "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -1686,7 +2297,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -1695,22 +2305,27 @@ } }, "node_modules/outvariant": { - "version": "1.4.2", - "dev": true, - "license": "MIT" + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.2", - "dev": true, - "license": "MIT" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true }, "node_modules/postcss": { - "version": "8.4.38", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -1726,28 +2341,56 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/rollup": { - "version": "4.18.0", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -1759,29 +2402,30 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, "node_modules/rxjs": { "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "peer": true, "dependencies": { "tslib": "^2.1.0" @@ -1789,8 +2433,9 @@ }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "ISC", "engines": { "node": ">=14" }, @@ -1799,30 +2444,34 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/statuses": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/strict-event-emitter": { "version": "0.5.1", - "dev": true, - "license": "MIT" + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true }, "node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1834,8 +2483,9 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -1845,8 +2495,9 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -1856,8 +2507,9 @@ }, "node_modules/tinymce": { "version": "6.8.4", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-6.8.4.tgz", + "integrity": "sha512-okoJyxuPv1gzASxQDNgQbnUXOdAIyoOSXcXcZZu7tiW0PSKEdf3SdASxPBupRj+64/E3elHwVRnzSdo82Emqbg==", "dev": true, - "license": "MIT", "peer": true }, "node_modules/tinymce-i18n": { @@ -1867,10 +2519,26 @@ "dev": true, "peer": true }, - "node_modules/tsconfck": { - "version": "3.1.0", + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsconfck": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.3.tgz", + "integrity": "sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==", "dev": true, - "license": "MIT", "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -1887,15 +2555,17 @@ } }, "node_modules/tslib": { - "version": "2.6.3", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "dev": true, - "license": "0BSD", "peer": true }, "node_modules/type-fest": { - "version": "4.20.1", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, @@ -1904,9 +2574,10 @@ } }, "node_modules/typescript": { - "version": "5.5.2", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, - "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1916,9 +2587,29 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } }, "node_modules/uuid": { "version": "10.0.0", @@ -1935,13 +2626,14 @@ } }, "node_modules/vite": { - "version": "5.3.1", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -1960,6 +2652,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -1977,6 +2670,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -1990,8 +2686,9 @@ }, "node_modules/vite-tsconfig-paths": { "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -2008,8 +2705,9 @@ }, "node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, - "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2021,16 +2719,18 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, - "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, - "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2046,11 +2746,24 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, - "license": "ISC", "engines": { "node": ">=12" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/src/Umbraco.Web.UI.Login/public/mockServiceWorker.js b/src/Umbraco.Web.UI.Login/public/mockServiceWorker.js index 24fe3a25f0..a8262f093f 100644 --- a/src/Umbraco.Web.UI.Login/public/mockServiceWorker.js +++ b/src/Umbraco.Web.UI.Login/public/mockServiceWorker.js @@ -8,7 +8,7 @@ * - Please do NOT serve this file on production. */ -const PACKAGE_VERSION = '2.3.1' +const PACKAGE_VERSION = '2.4.9' const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() From cd9979bed7f50aa1b2d2667ee863296b3eb08364 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:17:06 +0200 Subject: [PATCH 30/38] V15: Allows blocks in rich text editor to leave out the "Umbraco-Block" HTML comment (#17118) * feat: allows blocks in rte to leave out the "Umbraco-Block" comment from their bodies * test: add unit test to test blocks without html comments as well as inline blocks --- .../ValueConverters/RichTextParsingRegexes.cs | 2 +- .../RichTextPropertyEditorHelperTests.cs | 55 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs index 75851ee856..2d4c19c17a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs @@ -4,6 +4,6 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; internal static partial class RichTextParsingRegexes { - [GeneratedRegex(".[^\"]*)\"><\\/umb-rte-block(?:-inline)?>")] + [GeneratedRegex(".[^\"]*)\">(?:)?<\\/umb-rte-block(?:-inline)?>")] public static partial Regex BlockRegex(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs index 42bff3a868..71fa5f085c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs @@ -135,7 +135,7 @@ public class RichTextPropertyEditorHelperTests { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ @@ -157,7 +157,7 @@ public class RichTextPropertyEditorHelperTests var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -172,6 +172,57 @@ public class RichTextPropertyEditorHelperTests Assert.AreEqual(0, value.Blocks.SettingsData.Count); } + [Test] + public void Can_Parse_Mixed_Blocks_And_Inline_Blocks() + { + const string input = """ + { + "markup": "

this is some markup

", + "blocks": { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + }, { + "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03" + } + ] + }, + "contentData": [{ + "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", + "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "contentPropertyAlias": "A content property value" + }, { + "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1124", + "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03", + "contentPropertyAlias": "A content property value" + } + ], + "settingsData": [] + } + } + """; + + var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); + Assert.IsTrue(result); + Assert.IsNotNull(value); + Assert.AreEqual("

this is some markup

", value.Markup); + + Assert.IsNotNull(value.Blocks); + + Guid[] contentTypeGuids = [Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"), Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1124")]; + Guid[] itemGuids = [Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"), Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf03")]; + + Assert.AreEqual(2, value.Blocks.ContentData.Count); + for (var i = 0; i < value.Blocks.ContentData.Count; i++) { + var item = value.Blocks.ContentData[i]; + Assert.AreEqual(contentTypeGuids[i], item.ContentTypeKey); + Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuids[i]), item.Udi); + Assert.AreEqual(itemGuids[i], item.Key); + } + + Assert.AreEqual(0, value.Blocks.SettingsData.Count); + } + private IJsonSerializer JsonSerializer() => new SystemTextJsonSerializer(); private ILogger Logger() => Mock.Of(); From 71d6b45a95c39d7442f0ce50d0e6fe67ee869eac Mon Sep 17 00:00:00 2001 From: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:33:23 +0200 Subject: [PATCH 31/38] V14 QA added navigation integration tests (#16973) * Tests * Remove props and use local vars * Adding preliminary navigation service and content implementation * Adding preliminary unit tests * Change from async methods * Refactor GetParentKey to TryGetParentKey * Refactor GetChildrenKeys to TryGetChildrenKeys * Refactor GetDescendantsKeys to TryGetDescendantsKeys * Refactor GetAncestorsKeys to TryGetAncestorsKeys * Refactor GetSiblingsKeys to TryGetSiblingsKeys * Refactor TryGetChildrenKeys * Initial integration tests * Use ContentEditingService instead of ContentService * Remove INavigationService.Copy implementation and unit tests * Rename var * Adding clarification * Initial ContentNavigationRepository * Initial NavigationFactory * Remove filtering from factory * NavigationRepository and implementation * InitializationService responsible for seeding the in-memory structure * Register repository and service * Adding NavigationDto and NavigationNode * Adding INavigationService dependency and Enlist updating navigation structure actions * Documentation * Adding tests for removing descendants as well * Changed to ConcurrentDictionary * Remove keys comments for tests * Adding documentation * Forgotten ConcurrentDictionary change * Isolating the operations on the model * Splitting the INavigationService to separate the querying from the managing functionality * Introducing specific navigation services for document, document recycle bin, media and media recycle bin * Making ContentNavigationService into a base as the functionality will be shared between the document, document recycle bin, media and media recycle bin services * Adding the implementations of document, document recycle bin, media and media recycle bin navigation services * Fixing comments * Initializing all 4 collections * Adapting the navigation unit tests to the base now * Adapting integration tests to specific navigation service * Adding test for rebuilding the structure * Adding implementation for Adding and Getting a node - needed for moving to and restoring from the recycle bin + tests * Updating the document navigation structure from the ContentService * Fix typo * Adding trashed items implementation in base - currently managing 2 structures * Removing no longer relevant GetNavigationNode and AddNavigationNode * Fix removing parent when child is removed supporting methods * Added restoring functionality * Adding Bin functionality to DocumentNavigationService * Removing Move signature from IDocumentNavigationService * Adding RecycleBin query and management services * Re-adding Move and removing GetNavigationNode and AddNavigationNode signatures from interface * Rebuilding bin structure using _documentNavigationService, instead of _documentRecycleBinNavigationService * Fixing test name * Adding more tests for remove * Adding tests for restore and removing ones for GetNavigationNode and AddNavigationNode * Remove comments * Removing document and media RecycleBinNavigationService and their interfaces * Adding media rebuild bin * Fixing initialization with correct interfaces * Removing RecycleBinNavigationServices' registration * Remove IDocumentRecycleBinNavigationService dependency * Updating in-memory nav structure when content updates happen * Adding the rest of the integration tests * Clean up IMediaNavigationService * Fix comments * Remove CustomTestSetup in integration tests as the structure is updated when content updates happen * Adding and fixing comments * Making RebuildBinAsync abstract as well * Adding DocumentNavigationServiceTestsBase * Splitting DocumentNavigationServiceTests into partial test classes * Cleaning up DocumentNavigationServiceTests since tests have been moved to specific partial classes * Reuse a method for creating content in tests * Change type in test base * Adding navigation structure updates in media service * Adding MediaNavigationServiceTestsBase * Adding integration tests for media nav str * Remove services as we will have more concrete ones * Add document and media IXNavigationQueryService and IXNavigationManagementService * Inject ManagementService in ContentService.cs and MediaService.cs * Change implementation to implement the new services + registration * Make classes sealed * Inject correct services in InitializationService * Using the right services in integration tests * Adding comments * Removing bin interfaces from main navigation ones * Rename Remove to MoveToBin * Added tests for Copy * Added additional tests * Split test * Added Media tests * Updated and added content tests * Cleaned up naming * Cleaned up * Rename initialization service to initialization hosted service * Refactor repository to return a collection * Add interface for the NavigationDto * Add constants to bind property names between DTOs * Move factory and fix input type * Use constants for column names * Use factory from base * Fixed indentation * Fix bug when rebuilding the recycle bin structure * Fix comments * Fix merged in code * Fix bug again after merge * Minor things --------- Co-authored-by: Elitsa Co-authored-by: Bjarke Berg --- .../Factories/NavigationFactory.cs | 4 +- .../ContentNavigationServiceBase.cs | 37 ++++---- .../Navigation/INavigationQueryService.cs | 3 +- .../DocumentNavigationServiceTests.Copy.cs | 95 +++++++++++++++---- .../DocumentNavigationServiceTests.Create.cs | 35 +++++-- .../DocumentNavigationServiceTests.Delete.cs | 1 - ...gationServiceTests.DeleteFromRecycleBin.cs | 22 ++++- .../DocumentNavigationServiceTests.Move.cs | 80 +++++++++++++--- ...NavigationServiceTests.MoveToRecycleBin.cs | 21 +++- .../DocumentNavigationServiceTests.Rebuild.cs | 46 ++++++++- .../DocumentNavigationServiceTests.Restore.cs | 7 +- .../MediaNavigationServiceTests.Create.cs | 48 ++++++++++ .../MediaNavigationServiceTests.Delete.cs | 31 ++++++ ...gationServiceTests.DeleteFromRecycleBin.cs | 33 +++++++ .../MediaNavigationServiceTests.Move.cs | 88 +++++++++++++++++ ...NavigationServiceTests.MoveToRecycleBin.cs | 39 ++++++++ .../MediaNavigationServiceTests.Rebuild.cs | 46 ++++++++- .../MediaNavigationServiceTests.Restore.cs | 44 +++++++++ .../MediaNavigationServiceTests.Update.cs | 44 +++++++++ 19 files changed, 654 insertions(+), 70 deletions(-) diff --git a/src/Umbraco.Core/Factories/NavigationFactory.cs b/src/Umbraco.Core/Factories/NavigationFactory.cs index 815312e048..a95cbf68a5 100644 --- a/src/Umbraco.Core/Factories/NavigationFactory.cs +++ b/src/Umbraco.Core/Factories/NavigationFactory.cs @@ -11,10 +11,10 @@ internal static class NavigationFactory ///
/// A dictionary of objects with key corresponding to their unique Guid. /// The objects used to build the navigation nodes dictionary. - public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure,IEnumerable entities) + public static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IEnumerable entities) { var entityList = entities.ToList(); - var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); + Dictionary idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key); foreach (INavigationModel entity in entityList) { diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs index 394223c311..4f2d1e1c9b 100644 --- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs +++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs @@ -33,12 +33,12 @@ internal abstract class ContentNavigationServiceBase public bool TryGetParentKey(Guid childKey, out Guid? parentKey) => TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey); + public bool TryGetRootKeys(out IEnumerable rootKeys) + => TryGetRootKeysFromStructure(_navigationStructure, out rootKeys); + public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys) => TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys); - public bool TryGetRootKeys(out IEnumerable childrenKeys) - => TryGetRootKeysFromStructure(_navigationStructure, out childrenKeys); - public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys) => TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys); @@ -165,7 +165,6 @@ internal abstract class ContentNavigationServiceBase _recycleBinNavigationStructure.TryRemove(key, out _); } - /// /// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed. /// Only relevant for items in the content and media trees (which have readLock values of -333 or -334). @@ -184,11 +183,17 @@ internal abstract class ContentNavigationServiceBase using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true); scope.ReadLock(readLock); - IEnumerable navigationModels = trashed ? - _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) : - _navigationRepository.GetContentNodesByObjectType(objectTypeKey); - - NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels); + // Build the corresponding navigation structure + if (trashed) + { + IEnumerable navigationModels = _navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey); + NavigationFactory.BuildNavigationDictionary(_recycleBinNavigationStructure, navigationModels); + } + else + { + IEnumerable navigationModels = _navigationRepository.GetContentNodesByObjectType(objectTypeKey); + NavigationFactory.BuildNavigationDictionary(_navigationStructure, navigationModels); + } } private bool TryGetParentKeyFromStructure(ConcurrentDictionary structure, Guid childKey, out Guid? parentKey) @@ -204,6 +209,13 @@ internal abstract class ContentNavigationServiceBase return false; } + private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable rootKeys) + { + // TODO can we make this more efficient? + rootKeys = structure.Values.Where(x => x.Parent is null).Select(x => x.Key); + return true; + } + private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable childrenKeys) { if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false) @@ -217,13 +229,6 @@ internal abstract class ContentNavigationServiceBase return true; } - private bool TryGetRootKeysFromStructure(ConcurrentDictionary structure, out IEnumerable childrenKeys) - { - // TODO can we make this more efficient? - childrenKeys = structure.Values.Where(x=>x.Parent is null).Select(x=>x.Key); - return true; - } - private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary structure, Guid parentKey, out IEnumerable descendantsKeys) { var descendants = new List(); diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs index 9b6fb9807d..204ec657eb 100644 --- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs +++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs @@ -8,8 +8,9 @@ public interface INavigationQueryService { bool TryGetParentKey(Guid childKey, out Guid? parentKey); + bool TryGetRootKeys(out IEnumerable rootKeys); + bool TryGetChildrenKeys(Guid parentKey, out IEnumerable childrenKeys); - bool TryGetRootKeys(out IEnumerable childrenKeys); bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable descendantsKeys); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs index b3ebfa2215..95659b38be 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs @@ -3,17 +3,12 @@ using Umbraco.Cms.Core; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -// TODO: test that it is added to its new parent - check parent's children -// TODO: test that it has the same amount of descendants - depending on value of includeDescendants param -// TODO: test that the number of target parent descendants updates when copying node with descendants -// TODO: test that copied node descendants have different keys than source node descendants public partial class DocumentNavigationServiceTests { [Test] [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB", "A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2 to itself - [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", null)] // Child 2 to content root [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 3 to Child 1 - public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid? targetParentKey) + public async Task Structure_Updates_When_Copying_Content(Guid nodeToCopy, Guid targetParentKey) { // Arrange DocumentNavigationQueryService.TryGetParentKey(nodeToCopy, out Guid? sourceParentKey); @@ -29,18 +24,86 @@ public partial class DocumentNavigationServiceTests Assert.Multiple(() => { - if (targetParentKey is null) - { - // Verify the copied node's parent is null (it's been copied to content root) - Assert.IsNull(copiedItemParentKey); - } - else - { - Assert.IsNotNull(copiedItemParentKey); - } - + Assert.IsNotNull(copiedItemParentKey); Assert.AreEqual(targetParentKey, copiedItemParentKey); Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); }); } + + [Test] + public async Task Structure_Updates_When_Copying_Content_To_Root() + { + // Arrange + DocumentNavigationQueryService.TryGetParentKey(Grandchild2.Key, out Guid? sourceParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable beforeCopyRootSiblingsKeys); + var initialRootSiblingsCount = beforeCopyRootSiblingsKeys.Count(); + + // Act + var copyAttempt = await ContentEditingService.CopyAsync(Grandchild2.Key, null, false, false, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; + + // Assert + Assert.AreNotEqual(Grandchild2.Key, copiedItemKey); + + DocumentNavigationQueryService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable afterCopyRootSiblingsKeys); + DocumentNavigationQueryService.TryGetChildrenKeys(sourceParentKey.Value, out IEnumerable sourceParentChildrenKeys); + List rootSiblingsList = afterCopyRootSiblingsKeys.ToList(); + + Assert.Multiple(() => + { + // Verifies that the node actually has been copied + Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); + Assert.IsNull(copiedItemParentKey); + + // Verifies that the siblings amount has been updated after copying + Assert.AreEqual(initialRootSiblingsCount + 1, rootSiblingsList.Count); + Assert.IsTrue(rootSiblingsList.Contains(copiedItemKey)); + + // Verifies that the node was copied and not moved + Assert.IsTrue(sourceParentChildrenKeys.Contains(Grandchild2.Key)); + }); + } + + [Test] + public async Task Structure_Updates_When_Copying_Content_With_Descendants() + { + // Arrange + DocumentNavigationQueryService.TryGetParentKey(Grandchild3.Key, out Guid? sourceParentKey); + DocumentNavigationQueryService.TryGetDescendantsKeys(Grandchild3.Key, out IEnumerable beforeCopyGrandChild1Descendents); + DocumentNavigationQueryService.TryGetChildrenKeys(Child3.Key, out IEnumerable beforeCopyChild3ChildrenKeys); + var initialChild3ChildrenCount = beforeCopyChild3ChildrenKeys.Count(); + var initialGrandChild1DescendentsCount = beforeCopyGrandChild1Descendents.Count(); + + // Act + var copyAttempt = await ContentEditingService.CopyAsync(Grandchild3.Key, Child3.Key, false, true, Constants.Security.SuperUserKey); + Guid copiedItemKey = copyAttempt.Result.Key; + + // Assert + Assert.AreNotEqual(Grandchild3.Key, copiedItemKey); + + DocumentNavigationQueryService.TryGetParentKey(copiedItemKey, out Guid? copiedItemParentKey); + DocumentNavigationQueryService.TryGetChildrenKeys(Child3.Key, out IEnumerable afterCopyChild3ChildrenKeys); + DocumentNavigationQueryService.TryGetChildrenKeys(copiedItemKey, out IEnumerable afterCopyGrandChild1Descendents); + List child3ChildrenList = afterCopyChild3ChildrenKeys.ToList(); + List grandChild1DescendantsList = afterCopyGrandChild1Descendents.ToList(); + + // Retrieves the child of the copied item to check its content + var copiedGreatGrandChild1 = await ContentEditingService.GetAsync(grandChild1DescendantsList.First()); + + Assert.Multiple(() => + { + // Verifies that the node actually has been copied + Assert.AreNotEqual(sourceParentKey, copiedItemParentKey); + Assert.AreEqual(Child3.Key, copiedItemParentKey); + Assert.AreEqual(initialChild3ChildrenCount + 1, child3ChildrenList.Count); + + // Verifies that the descendant amount is the same for the original and the moved GrandChild1 node + Assert.AreEqual(initialGrandChild1DescendentsCount, grandChild1DescendantsList.Count); + + // Verifies that the keys are not the same + Assert.AreEqual(GreatGrandchild1.Name, copiedGreatGrandChild1.Name); + Assert.AreNotEqual(GreatGrandchild1.Key, copiedGreatGrandChild1.Key); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs index 2ee6c7cabe..4138f80b07 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs @@ -7,18 +7,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class DocumentNavigationServiceTests { [Test] - public async Task Structure_Updates_When_Creating_Content() + public async Task Structure_Updates_When_Creating_Content_At_Root() { // Arrange DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable initialSiblingsKeys); var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); - - var createModel = new ContentCreateModel - { - ContentTypeKey = ContentType.Key, - ParentKey = Constants.System.RootKey, // Create node at content root - InvariantName = "Root 2", - }; + var createModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey); // Act var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); @@ -36,4 +30,29 @@ public partial class DocumentNavigationServiceTests Assert.AreEqual(createdItemKey, siblingsList.First()); }); } + + [Test] + public async Task Structure_Updates_When_Creating_Child_Content() + { + // Arrange + DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable initialChildrenKeys); + var initialChild1ChildrenCount = initialChildrenKeys.Count(); + var createModel = CreateContentCreateModel("Child1Child", Guid.NewGuid(), Child1.Key); + + // Act + var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the children of the Child1 node once again + DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable updatedChildrenKeys); + List childrenList = updatedChildrenKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(childrenList); + Assert.AreEqual(initialChild1ChildrenCount + 1, childrenList.Count); + Assert.IsTrue(childrenList.Contains(createdItemKey)); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs index 5e6e655d74..8a60eaecc8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs @@ -3,7 +3,6 @@ using Umbraco.Cms.Core; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -// TODO: Test that the descendants of the node have also been removed from both structures public partial class DocumentNavigationServiceTests { [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs index 1f9b819366..ee9f2815aa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.DeleteFromRecycleBin.cs @@ -3,7 +3,6 @@ using Umbraco.Cms.Core; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -// TODO: check that the descendants have also been removed from both structures - navigation and trash public partial class DocumentNavigationServiceTests { [Test] @@ -12,11 +11,14 @@ public partial class DocumentNavigationServiceTests // Arrange Guid nodeToDelete = Child1.Key; Guid nodeInRecycleBin = Grandchild4.Key; + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); // Move nodes to recycle bin - await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); // Make sure we have an item already in the recycle bin to act as a sibling - await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); + await ContentEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + var initialSiblingsCount = initialSiblingsKeys.Count(); + Assert.AreEqual(initialSiblingsCount, 1); // Act await ContentEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); @@ -24,7 +26,17 @@ public partial class DocumentNavigationServiceTests // Assert DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); - // Verify siblings count has decreased by one - Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsCount - 1, updatedSiblingsKeys.Count()); + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = DocumentNavigationQueryService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + var descendantExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); + } + }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs index 078e06de2b..f31f4f7907 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs @@ -7,12 +7,17 @@ public partial class DocumentNavigationServiceTests { [Test] [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573", "60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Grandchild 1 to Child 2 - [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF", null)] // Child 3 to content root [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96", "C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 2 to Child 1 - public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid? targetParentKey) + public async Task Structure_Updates_When_Moving_Content(Guid nodeToMove, Guid targetParentKey) { // Arrange DocumentNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable beforeMoveInitialParentChildrenKeys); + var beforeMoveInitialParentChildren = beforeMoveInitialParentChildrenKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey, out IEnumerable beforeMoveTargetParentChildrenKeys); + var beforeMoveTargetParentChildren = beforeMoveTargetParentChildrenKeys.ToList(); // Act var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); @@ -21,19 +26,72 @@ public partial class DocumentNavigationServiceTests DocumentNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); // Assert + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable afterMoveInitialParentChildrenKeys); + var afterMoveInitialParentChildren = afterMoveInitialParentChildrenKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey, out IEnumerable afterMoveTargetParentChildrenKeys); + var afterMoveTargetParentChildren = afterMoveTargetParentChildrenKeys.ToList(); + Assert.Multiple(() => { - if (targetParentKey is null) - { - Assert.IsNull(updatedParentKey); - } - else - { - Assert.IsNotNull(updatedParentKey); - } - + Assert.IsNotNull(updatedParentKey); Assert.AreNotEqual(originalParentKey, updatedParentKey); Assert.AreEqual(targetParentKey, updatedParentKey); + + // Verifies that the parent's children have been updated + Assert.AreEqual(beforeMoveInitialParentChildren.Count - 1, afterMoveInitialParentChildren.Count); + Assert.AreEqual(beforeMoveTargetParentChildren.Count + 1, afterMoveTargetParentChildren.Count); + + // Verifies that the descendants are the same before and after the move + Assert.AreEqual(beforeMoveDescendants.Count, afterMoveDescendants.Count); + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); + }); + } + + [Test] + public async Task Structure_Updates_When_Moving_Content_To_Root() + { + // Arrange + Guid nodeToMove = Child3.Key; + Guid? targetParentKey = Constants.System.RootKey; // Root + DocumentNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetDescendantsKeys(originalParentKey.Value, out IEnumerable beforeMoveInitialParentDescendantsKeys); + var beforeMoveInitialParentDescendants = beforeMoveInitialParentDescendantsKeys.ToList(); + + // The Root node is the only node at the root + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable beforeMoveSiblingsKeys); + var beforeMoveRootSiblings = beforeMoveSiblingsKeys.ToList(); + + // Act + var moveAttempt = await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + DocumentNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + + // Assert + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetDescendantsKeys((Guid)originalParentKey, out IEnumerable afterMoveInitialParentDescendantsKeys); + var afterMoveInitialParentDescendants = afterMoveInitialParentDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetSiblingsKeys(Root.Key, out IEnumerable afterMoveSiblingsKeys); + var afterMoveRootSiblings = afterMoveSiblingsKeys.ToList(); + + Assert.Multiple(() => + { + Assert.IsNull(updatedParentKey); + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + + // Verifies that the parent's children have been updated + Assert.AreEqual(beforeMoveInitialParentDescendants.Count - (afterMoveDescendants.Count + 1), afterMoveInitialParentDescendants.Count); + Assert.AreEqual(beforeMoveRootSiblings.Count + 1, afterMoveRootSiblings.Count); + + // Verifies that the descendants are the same before and after the move + Assert.AreEqual(beforeMoveDescendants.Count, afterMoveDescendants.Count); + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs index 1bd4bd9d83..9e5e7dcc69 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs @@ -3,8 +3,6 @@ using Umbraco.Cms.Core; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -// TODO: also check that initial siblings count's decreased -// TODO: and that descendants are still the same (i.e. they've also been moved to recycle bin) public partial class DocumentNavigationServiceTests { [Test] @@ -12,7 +10,16 @@ public partial class DocumentNavigationServiceTests { // Arrange Guid nodeToMoveToRecycleBin = Child3.Key; + Guid nodeInRecycleBin = Grandchild4.Key; + await ContentEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + var beforeMoveRecycleBinSiblingsCount = initialSiblingsKeys.Count(); + Assert.AreEqual(beforeMoveRecycleBinSiblingsCount, 0); DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMoveToRecycleBin, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable initialParentChildrenKeys); + var beforeMoveParentSiblingsCount = initialParentChildrenKeys.Count(); // Act await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); @@ -20,13 +27,23 @@ public partial class DocumentNavigationServiceTests // Assert var nodeExists = DocumentNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure var nodeExistsInRecycleBin = DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToMoveToRecycleBin, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + DocumentNavigationQueryService.TryGetChildrenKeys((Guid)originalParentKey, out IEnumerable afterMoveParentChildrenKeys); + var afterMoveParentSiblingsCount = afterMoveParentChildrenKeys.Count(); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable afterMoveRecycleBinSiblingsKeys); + var afterMoveRecycleBinSiblingsCount = afterMoveRecycleBinSiblingsKeys.Count(); Assert.Multiple(() => { Assert.IsFalse(nodeExists); Assert.IsTrue(nodeExistsInRecycleBin); Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); + Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); + Assert.AreEqual(beforeMoveParentSiblingsCount - 1, afterMoveParentSiblingsCount); + Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount); }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs index 6d70870a32..addf668cb3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Rebuild.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Navigation; @@ -13,14 +14,14 @@ public partial class DocumentNavigationServiceTests // Arrange Guid nodeKey = Root.Key; - // Capture original built state of DocumentNavigationService + // Capture original built state of DocumentNavigationQueryService DocumentNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); DocumentNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); DocumentNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); DocumentNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); DocumentNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); - // Im-memory navigation structure is empty here + // In-memory navigation structure is empty here var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); var initialNodeExists = newDocumentNavigationService.TryGetParentKey(nodeKey, out _); @@ -52,8 +53,47 @@ public partial class DocumentNavigationServiceTests } [Test] - // TODO: Test that you can rebuild bin structure as well public async Task Bin_Structure_Can_Rebuild() { + // Arrange + Guid nodeKey = Root.Key; + await ContentEditingService.MoveToRecycleBinAsync(nodeKey, Constants.Security.SuperUserKey); + + // Capture original built state of DocumentNavigationQueryService + DocumentNavigationQueryService.TryGetParentKeyInBin(nodeKey, out Guid? originalParentKey); + DocumentNavigationQueryService.TryGetChildrenKeysInBin(nodeKey, out IEnumerable originalChildrenKeys); + DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeKey, out IEnumerable originalDescendantsKeys); + DocumentNavigationQueryService.TryGetAncestorsKeysInBin(nodeKey, out IEnumerable originalAncestorsKeys); + DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); + + // In-memory navigation structure is empty here + var newDocumentNavigationService = new DocumentNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newDocumentNavigationService.TryGetParentKeyInBin(nodeKey, out _); + + // Act + await newDocumentNavigationService.RebuildBinAsync(); + + // Capture rebuilt state + var nodeExists = newDocumentNavigationService.TryGetParentKeyInBin(nodeKey, out Guid? parentKeyFromRebuild); + newDocumentNavigationService.TryGetChildrenKeysInBin(nodeKey, out IEnumerable childrenKeysFromRebuild); + newDocumentNavigationService.TryGetDescendantsKeysInBin(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newDocumentNavigationService.TryGetAncestorsKeysInBin(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newDocumentNavigationService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of DocumentNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs index 3151fb83e4..e57f0c652c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs @@ -3,7 +3,6 @@ using Umbraco.Cms.Core; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; -// TODO: test that descendants are also restored in the right place public partial class DocumentNavigationServiceTests { [Test] @@ -20,6 +19,8 @@ public partial class DocumentNavigationServiceTests await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); // Make sure the item is in the recycle bin DocumentNavigationQueryService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToRestore, out IEnumerable initialDescendantsKeys); + var beforeRestoreDescendants = initialDescendantsKeys.ToList(); // Act var restoreAttempt = await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); @@ -28,7 +29,8 @@ public partial class DocumentNavigationServiceTests // Assert DocumentNavigationQueryService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); - + DocumentNavigationQueryService.TryGetDescendantsKeys(restoredItemKey, out IEnumerable afterRestoreDescendantsKeys); + var afterRestoreDescendants = afterRestoreDescendantsKeys.ToList(); Assert.Multiple(() => { // Verify siblings count has decreased by one @@ -44,6 +46,7 @@ public partial class DocumentNavigationServiceTests Assert.AreNotEqual(initialParentKey, restoredItemParentKey); } + Assert.AreEqual(beforeRestoreDescendants, afterRestoreDescendants); Assert.AreEqual(targetParentKey, restoredItemParentKey); }); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs index 1b3b6551a5..3111e5c8e5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs @@ -5,5 +5,53 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + public async Task Structure_Updates_When_Creating_Media_At_Root() + { + // Arrange + MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable initialSiblingsKeys); + var initialRootNodeSiblingsCount = initialSiblingsKeys.Count(); + var createModel = CreateMediaCreateModel("Root Image", Guid.NewGuid(), ImageMediaType.Key); + // Act + var createAttempt = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the siblings list of the Root once again + MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable updatedSiblingsKeys); + List siblingsList = updatedSiblingsKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(siblingsList); + Assert.AreEqual(initialRootNodeSiblingsCount + 1, siblingsList.Count); + Assert.AreEqual(createdItemKey, siblingsList.First()); + }); + } + + [Test] + public async Task Structure_Updates_When_Creating_Child_Media() + { + // Arrange + MediaNavigationQueryService.TryGetChildrenKeys(Album.Key, out IEnumerable initialChildrenKeys); + var initialChild1ChildrenCount = initialChildrenKeys.Count(); + var createModel = CreateMediaCreateModel("Child Image", Guid.NewGuid(), ImageMediaType.Key, Album.Key); + + // Act + var createAttempt = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Guid createdItemKey = createAttempt.Result.Content!.Key; + + // Verify that the structure has updated by checking the children of the Child1 node once again + MediaNavigationQueryService.TryGetChildrenKeys(Album.Key, out IEnumerable updatedChildrenKeys); + List childrenList = updatedChildrenKeys.ToList(); + + // Assert + Assert.Multiple(() => + { + Assert.IsNotEmpty(childrenList); + Assert.AreEqual(initialChild1ChildrenCount + 1, childrenList.Count); + Assert.IsTrue(childrenList.Contains(createdItemKey)); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs index 1b3b6551a5..5eeadb2484 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs @@ -5,5 +5,36 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + [TestCase("1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Album + [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2 + [TestCase("3E489C32-9315-42DA-95CE-823D154B09C8")] // Image 2 + public async Task Structure_Updates_When_Deleting_Media(Guid nodeToDelete) + { + // Arrange + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + // Act + // Deletes the item whether it is in the recycle bin or not + var deleteAttempt = await MediaEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey); + Guid deletedItemKey = deleteAttempt.Result.Key; + + // Assert + var nodeExists = MediaNavigationQueryService.TryGetDescendantsKeys(deletedItemKey, out _); + var nodeExistsInRecycleBin = MediaNavigationQueryService.TryGetDescendantsKeysInBin(nodeToDelete, out _); + + Assert.Multiple(() => + { + Assert.AreEqual(nodeToDelete, deletedItemKey); + Assert.IsFalse(nodeExists); + Assert.IsFalse(nodeExistsInRecycleBin); + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = MediaNavigationQueryService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + var descendantExistsInRecycleBin = MediaNavigationQueryService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); + } + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs index 1b3b6551a5..c5a69b30f3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.DeleteFromRecycleBin.cs @@ -5,5 +5,38 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + public async Task Structure_Updates_When_Deleting_From_Recycle_Bin() + { + // Arrange + Guid nodeToDelete = Image1.Key; + Guid nodeInRecycleBin = Image2.Key; + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToDelete, out IEnumerable initialDescendantsKeys); + // Move nodes to recycle bin + await MediaEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); + await MediaEditingService.MoveToRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + var initialSiblingsCount = initialSiblingsKeys.Count(); + Assert.AreEqual(initialSiblingsCount, 1); + + // Act + await MediaEditingService.DeleteFromRecycleBinAsync(nodeToDelete, Constants.Security.SuperUserKey); + + // Assert + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsCount - 1, updatedSiblingsKeys.Count()); + foreach (Guid descendant in initialDescendantsKeys) + { + var descendantExists = MediaNavigationQueryService.TryGetParentKey(descendant, out _); + Assert.IsFalse(descendantExists); + var descendantExistsInRecycleBin = MediaNavigationQueryService.TryGetParentKeyInBin(descendant, out _); + Assert.IsFalse(descendantExistsInRecycleBin); + } + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs index 1b3b6551a5..1677dcec45 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs @@ -5,5 +5,93 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + [TestCase("62BCE72F-8C18-420E-BCAC-112B5ECC95FD", "139DC977-E50F-4382-9728-B278C4B7AC6A")] // Image 4 to Sub-album 1 + [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB", "1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Sub-sub-album 1 to Album + public async Task Structure_Updates_When_Moving_Media(Guid nodeToMove, Guid targetParentKey) + { + // Arrange + MediaNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetDescendantsKeys(originalParentKey.Value, out IEnumerable beforeMoveInitialParentDescendantsKeys); + var beforeMoveInitialParentDescendants = beforeMoveInitialParentDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey, out IEnumerable beforeMoveTargetParentChildrenKeys); + var beforeMoveTargetParentChildren = beforeMoveTargetParentChildrenKeys.ToList(); + // Act + var moveAttempt = await MediaEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + MediaNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + + // Assert + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetDescendantsKeys(originalParentKey.Value, out IEnumerable afterMoveInitialParentDescendantsKeys); + var afterMoveInitialParentDescendants = afterMoveInitialParentDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey, out IEnumerable afterMoveTargetParentChildrenKeys); + var afterMoveTargetParentChildren = afterMoveTargetParentChildrenKeys.ToList(); + + Assert.Multiple(() => + { + Assert.IsNotNull(updatedParentKey); + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + + // Verifies that the parent's children have been updated + Assert.AreEqual(beforeMoveInitialParentDescendants.Count - (afterMoveDescendants.Count + 1), afterMoveInitialParentDescendants.Count); + Assert.AreEqual(beforeMoveTargetParentChildren.Count + 1, afterMoveTargetParentChildren.Count); + + // Verifies that the descendants are the same before and after the move + Assert.AreEqual(beforeMoveDescendants.Count, afterMoveDescendants.Count); + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); + }); + } + + [Test] + public async Task Structure_Updates_When_Moving_Media_To_Root() + { + // Arrange + Guid nodeToMove = SubAlbum2.Key; + Guid? targetParentKey = Constants.System.RootKey; + MediaNavigationQueryService.TryGetParentKey(nodeToMove, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetDescendantsKeys(originalParentKey.Value, out IEnumerable beforeMoveInitialParentDescendantsKeys); + var beforeMoveInitialParentDescendants = beforeMoveInitialParentDescendantsKeys.ToList(); + + // The Root node is the only node at the root + MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable beforeMoveSiblingsKeys); + var beforeMoveRootSiblings = beforeMoveSiblingsKeys.ToList(); + + // Act + var moveAttempt = await MediaEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey); + + // Verify the node's new parent is updated + MediaNavigationQueryService.TryGetParentKey(moveAttempt.Result!.Key, out Guid? updatedParentKey); + + // Assert + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToMove, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetDescendantsKeys((Guid)originalParentKey, out IEnumerable afterMoveInitialParentDescendantsKeys); + var afterMoveInitialParentDescendants = afterMoveInitialParentDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable afterMoveSiblingsKeys); + var afterMoveRootSiblings = afterMoveSiblingsKeys.ToList(); + + Assert.Multiple(() => + { + Assert.IsNull(updatedParentKey); + Assert.AreNotEqual(originalParentKey, updatedParentKey); + Assert.AreEqual(targetParentKey, updatedParentKey); + + // Verifies that the parent's children have been updated + Assert.AreEqual(beforeMoveInitialParentDescendants.Count - (afterMoveDescendants.Count + 1), afterMoveInitialParentDescendants.Count); + Assert.AreEqual(beforeMoveRootSiblings.Count + 1, afterMoveRootSiblings.Count); + + // Verifies that the descendants are the same before and after the move + Assert.AreEqual(beforeMoveDescendants.Count, afterMoveDescendants.Count); + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs index 1b3b6551a5..f54eae0924 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs @@ -5,5 +5,44 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + public async Task Structure_Updates_When_Moving_Media_To_Recycle_Bin() + { + // Arrange + Guid nodeToMoveToRecycleBin = Image3.Key; + Guid nodeInRecycleBin = SubSubAlbum1.Key; + await MediaEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + var beforeMoveRecycleBinSiblingsCount = initialSiblingsKeys.Count(); + Assert.AreEqual(beforeMoveRecycleBinSiblingsCount, 0); + MediaNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToMoveToRecycleBin, out IEnumerable initialDescendantsKeys); + var beforeMoveDescendants = initialDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable initialParentChildrenKeys); + var beforeMoveParentSiblingsCount = initialParentChildrenKeys.Count(); + // Act + await MediaEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey); + + // Assert + var nodeExists = MediaNavigationQueryService.TryGetParentKey(nodeToMoveToRecycleBin, out _); // Verify that the item is no longer in the document structure + var nodeExistsInRecycleBin = MediaNavigationQueryService.TryGetParentKeyInBin(nodeToMoveToRecycleBin, out Guid? updatedParentKeyInRecycleBin); + MediaNavigationQueryService.TryGetDescendantsKeysInBin(nodeToMoveToRecycleBin, out IEnumerable afterMoveDescendantsKeys); + var afterMoveDescendants = afterMoveDescendantsKeys.ToList(); + MediaNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable afterMoveParentChildrenKeys); + var afterMoveParentSiblingsCount = afterMoveParentChildrenKeys.Count(); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable afterMoveRecycleBinSiblingsKeys); + var afterMoveRecycleBinSiblingsCount = afterMoveRecycleBinSiblingsKeys.Count(); + + Assert.Multiple(() => + { + Assert.IsFalse(nodeExists); + Assert.IsTrue(nodeExistsInRecycleBin); + Assert.AreNotEqual(originalParentKey, updatedParentKeyInRecycleBin); + Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null) + Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants); + Assert.AreEqual(beforeMoveParentSiblingsCount - 1, afterMoveParentSiblingsCount); + Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs index 0f36e8b8ec..65244743e3 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Rebuild.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Navigation; @@ -13,14 +14,14 @@ public partial class MediaNavigationServiceTests // Arrange Guid nodeKey = Album.Key; - // Capture original built state of MediaNavigationService + // Capture original built state of MediaNavigationQueryService MediaNavigationQueryService.TryGetParentKey(nodeKey, out Guid? originalParentKey); MediaNavigationQueryService.TryGetChildrenKeys(nodeKey, out IEnumerable originalChildrenKeys); MediaNavigationQueryService.TryGetDescendantsKeys(nodeKey, out IEnumerable originalDescendantsKeys); MediaNavigationQueryService.TryGetAncestorsKeys(nodeKey, out IEnumerable originalAncestorsKeys); MediaNavigationQueryService.TryGetSiblingsKeys(nodeKey, out IEnumerable originalSiblingsKeys); - // Im-memory navigation structure is empty here + // In-memory navigation structure is empty here var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); var initialNodeExists = newMediaNavigationService.TryGetParentKey(nodeKey, out _); @@ -52,8 +53,47 @@ public partial class MediaNavigationServiceTests } [Test] - // TODO: Test that you can rebuild bin structure as well public async Task Bin_Structure_Can_Rebuild() { + // Arrange + Guid nodeKey = Album.Key; + await MediaEditingService.MoveToRecycleBinAsync(nodeKey, Constants.Security.SuperUserKey); + + // Capture original built state of MediaNavigationQueryService + MediaNavigationQueryService.TryGetParentKeyInBin(nodeKey, out Guid? originalParentKey); + MediaNavigationQueryService.TryGetChildrenKeysInBin(nodeKey, out IEnumerable originalChildrenKeys); + MediaNavigationQueryService.TryGetDescendantsKeysInBin(nodeKey, out IEnumerable originalDescendantsKeys); + MediaNavigationQueryService.TryGetAncestorsKeysInBin(nodeKey, out IEnumerable originalAncestorsKeys); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable originalSiblingsKeys); + + // In-memory navigation structure is empty here + var newMediaNavigationService = new MediaNavigationService(GetRequiredService(), GetRequiredService()); + var initialNodeExists = newMediaNavigationService.TryGetParentKeyInBin(nodeKey, out _); + + // Act + await newMediaNavigationService.RebuildBinAsync(); + + // Capture rebuilt state + var nodeExists = newMediaNavigationService.TryGetParentKeyInBin(nodeKey, out Guid? parentKeyFromRebuild); + newMediaNavigationService.TryGetChildrenKeysInBin(nodeKey, out IEnumerable childrenKeysFromRebuild); + newMediaNavigationService.TryGetDescendantsKeysInBin(nodeKey, out IEnumerable descendantsKeysFromRebuild); + newMediaNavigationService.TryGetAncestorsKeysInBin(nodeKey, out IEnumerable ancestorsKeysFromRebuild); + newMediaNavigationService.TryGetSiblingsKeysInBin(nodeKey, out IEnumerable siblingsKeysFromRebuild); + + // Assert + Assert.Multiple(() => + { + Assert.IsFalse(initialNodeExists); + + // Verify that the item is present in the navigation structure after a rebuild + Assert.IsTrue(nodeExists); + + // Verify that we have the same items as in the original built state of MediaNavigationService + Assert.AreEqual(originalParentKey, parentKeyFromRebuild); + CollectionAssert.AreEquivalent(originalChildrenKeys, childrenKeysFromRebuild); + CollectionAssert.AreEquivalent(originalDescendantsKeys, descendantsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalAncestorsKeys, ancestorsKeysFromRebuild); + CollectionAssert.AreEquivalent(originalSiblingsKeys, siblingsKeysFromRebuild); + }); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs index 1b3b6551a5..22e5e3d799 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs @@ -5,5 +5,49 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { + [Test] + [TestCase("62BCE72F-8C18-420E-BCAC-112B5ECC95FD", "139DC977-E50F-4382-9728-B278C4B7AC6A")] // Image 4 to Sub-album 1 + [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564", "1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Sub-album 2 to Album + [TestCase("3E489C32-9315-42DA-95CE-823D154B09C8", null)] // Image 2 to media root + public async Task Structure_Updates_When_Restoring_Media(Guid nodeToRestore, Guid? targetParentKey) + { + // Arrange + Guid nodeInRecycleBin = Image3.Key; + // Move nodes to recycle bin + await MediaEditingService.MoveToRecycleBinAsync(nodeInRecycleBin, Constants.Security.SuperUserKey); + await MediaEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey); + MediaNavigationQueryService.TryGetParentKeyInBin(nodeToRestore, out Guid? initialParentKey); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable initialSiblingsKeys); + MediaNavigationQueryService.TryGetDescendantsKeysInBin(nodeToRestore, out IEnumerable initialDescendantsKeys); + var beforeRestoreDescendants = initialDescendantsKeys.ToList(); + + // Act + var restoreAttempt = await MediaEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey); + Guid restoredItemKey = restoreAttempt.Result.Key; + + // Assert + MediaNavigationQueryService.TryGetParentKey(restoredItemKey, out Guid? restoredItemParentKey); + MediaNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable updatedSiblingsKeys); + MediaNavigationQueryService.TryGetDescendantsKeys(restoredItemKey, out IEnumerable afterRestoreDescendantsKeys); + var afterRestoreDescendants = afterRestoreDescendantsKeys.ToList(); + + Assert.Multiple(() => + { + // Verify siblings count has decreased by one + Assert.AreEqual(initialSiblingsKeys.Count() - 1, updatedSiblingsKeys.Count()); + if (targetParentKey is null) + { + Assert.IsNull(restoredItemParentKey); + } + else + { + Assert.IsNotNull(restoredItemParentKey); + Assert.AreNotEqual(initialParentKey, restoredItemParentKey); + } + + Assert.AreEqual(beforeRestoreDescendants, afterRestoreDescendants); + Assert.AreEqual(targetParentKey, restoredItemParentKey); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs index 1b3b6551a5..0c24dbd0c8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Update.cs @@ -1,9 +1,53 @@ using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.ContentEditing; namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; public partial class MediaNavigationServiceTests { +[Test] + public async Task Structure_Does_Not_Update_When_Updating_Media() + { + // Arrange + Guid nodeToUpdate = Album.Key; + // Capture initial state + MediaNavigationQueryService.TryGetParentKey(nodeToUpdate, out Guid? initialParentKey); + MediaNavigationQueryService.TryGetChildrenKeys(nodeToUpdate, out IEnumerable initialChildrenKeys); + MediaNavigationQueryService.TryGetDescendantsKeys(nodeToUpdate, out IEnumerable initialDescendantsKeys); + MediaNavigationQueryService.TryGetAncestorsKeys(nodeToUpdate, out IEnumerable initialAncestorsKeys); + MediaNavigationQueryService.TryGetSiblingsKeys(nodeToUpdate, out IEnumerable initialSiblingsKeys); + + var updateModel = new MediaUpdateModel + { + InvariantName = "Updated Album", + }; + + // Act + var updateAttempt = await MediaEditingService.UpdateAsync(nodeToUpdate, updateModel, Constants.Security.SuperUserKey); + Guid updatedItemKey = updateAttempt.Result.Content!.Key; + + // Capture updated state + var nodeExists = MediaNavigationQueryService.TryGetParentKey(updatedItemKey, out Guid? updatedParentKey); + MediaNavigationQueryService.TryGetChildrenKeys(updatedItemKey, out IEnumerable childrenKeysAfterUpdate); + MediaNavigationQueryService.TryGetDescendantsKeys(updatedItemKey, out IEnumerable descendantsKeysAfterUpdate); + MediaNavigationQueryService.TryGetAncestorsKeys(updatedItemKey, out IEnumerable ancestorsKeysAfterUpdate); + MediaNavigationQueryService.TryGetSiblingsKeys(updatedItemKey, out IEnumerable siblingsKeysAfterUpdate); + + // Assert + Assert.Multiple(() => + { + // Verify that the item is still present in the navigation structure + Assert.IsTrue(nodeExists); + Assert.AreEqual(nodeToUpdate, updatedItemKey); + + // Verify that nothing's changed + Assert.AreEqual(initialParentKey, updatedParentKey); + CollectionAssert.AreEquivalent(initialChildrenKeys, childrenKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialDescendantsKeys, descendantsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialAncestorsKeys, ancestorsKeysAfterUpdate); + CollectionAssert.AreEquivalent(initialSiblingsKeys, siblingsKeysAfterUpdate); + }); + } } From 474cffbf7c436f87ed837d731f076cc32d05f5d7 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 25 Sep 2024 10:59:55 +0200 Subject: [PATCH 32/38] Make AddUserClientId able to run twice if needed (#17123) --- .../Migrations/Upgrade/V_15_0_0/AddUserClientId.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddUserClientId.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddUserClientId.cs index 12322dd651..946ac29607 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddUserClientId.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/AddUserClientId.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; @@ -11,5 +12,12 @@ public class AddUserClientId : MigrationBase } protected override void Migrate() - => Create.Table().Do(); + { + if (TableExists(Constants.DatabaseSchema.Tables.User2ClientId)) + { + return; + } + + Create.Table().Do(); + } } From 548b5e41506fa0d03011153ec03df1edd77a140f Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 25 Sep 2024 15:34:14 +0200 Subject: [PATCH 33/38] Enable validation of specific cultures only for document updates (#17087) * Enable validation of specific cultures only for document updates * Only validate explicitly sent cultures in the create validation endpoint * Fix backwards compat (obsolete old method) --------- Co-authored-by: Mads Rasmussen --- .../ValidateUpdateDocumentController.cs | 28 ++- .../DocumentEditingPresentationFactory.cs | 14 +- .../IDocumentEditingPresentationFactory.cs | 2 + src/Umbraco.Cms.Api.Management/OpenApi.json | 189 ++++++++++++++++++ .../ValidateUpdateDocumentRequestModel.cs | 6 + .../ValidateContentUpdateModel.cs | 6 + .../Services/ContentEditingService.cs | 16 +- .../Services/ContentEditingServiceBase.cs | 10 +- .../Services/ContentValidationService.cs | 5 +- .../Services/ContentValidationServiceBase.cs | 5 +- .../Services/IContentEditingService.cs | 3 + .../Services/IContentValidationServiceBase.cs | 2 +- .../Services/MediaValidationService.cs | 5 +- .../Services/MemberValidationService.cs | 5 +- .../Services/ContentValidationServiceTests.cs | 105 ++++++++++ 15 files changed, 383 insertions(+), 18 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs create mode 100644 src/Umbraco.Core/Models/ContentEditing/ValidateContentUpdateModel.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs index f80203d135..05f029f582 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/ValidateUpdateDocumentController.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Services.OperationStatus; namespace Umbraco.Cms.Api.Management.Controllers.Document; [ApiVersion("1.0")] +[ApiVersion("1.1")] public class ValidateUpdateDocumentController : UpdateDocumentControllerBase { private readonly IContentEditingService _contentEditingService; @@ -32,10 +33,35 @@ public class ValidateUpdateDocumentController : UpdateDocumentControllerBase [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + [Obsolete("Please use version 1.1 of this API. Will be removed in V16.")] public async Task Validate(CancellationToken cancellationToken, Guid id, UpdateDocumentRequestModel requestModel) => await HandleRequest(id, requestModel, async () => { - ContentUpdateModel model = _documentEditingPresentationFactory.MapUpdateModel(requestModel); + var validateUpdateDocumentRequestModel = new ValidateUpdateDocumentRequestModel + { + Values = requestModel.Values, + Variants = requestModel.Variants, + Template = requestModel.Template, + Cultures = null + }; + + ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(validateUpdateDocumentRequestModel); + Attempt result = await _contentEditingService.ValidateUpdateAsync(id, model); + + return result.Success + ? Ok() + : DocumentEditingOperationStatusResult(result.Status, requestModel, result.Result); + }); + + [HttpPut("{id:guid}/validate")] + [MapToApiVersion("1.1")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)] + public async Task ValidateV1_1(CancellationToken cancellationToken, Guid id, ValidateUpdateDocumentRequestModel requestModel) + => await HandleRequest(id, requestModel, async () => + { + ValidateContentUpdateModel model = _documentEditingPresentationFactory.MapValidateUpdateModel(requestModel); Attempt result = await _contentEditingService.ValidateUpdateAsync(id, model); return result.Success diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs index 4f96cdb099..31b1a9a66f 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentEditingPresentationFactory.cs @@ -17,8 +17,20 @@ internal sealed class DocumentEditingPresentationFactory : ContentEditingPresent } public ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel) + => MapUpdateContentModel(requestModel); + + public ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel) { - ContentUpdateModel model = MapContentEditingModel(requestModel); + ValidateContentUpdateModel model = MapUpdateContentModel(requestModel); + model.Cultures = requestModel.Cultures; + + return model; + } + + private TUpdateModel MapUpdateContentModel(UpdateDocumentRequestModel requestModel) + where TUpdateModel : ContentUpdateModel, new() + { + TUpdateModel model = MapContentEditingModel(requestModel); model.TemplateKey = requestModel.Template?.Id; return model; diff --git a/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs index dbc8538563..52978698d4 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/IDocumentEditingPresentationFactory.cs @@ -8,4 +8,6 @@ public interface IDocumentEditingPresentationFactory ContentCreateModel MapCreateModel(CreateDocumentRequestModel requestModel); ContentUpdateModel MapUpdateModel(UpdateDocumentRequestModel requestModel); + + ValidateContentUpdateModel MapValidateUpdateModel(ValidateUpdateDocumentRequestModel requestModel); } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index af7ce7081a..29acbdd16a 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -9372,6 +9372,149 @@ } } }, + "deprecated": true, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, + "/umbraco/management/api/v1.1/document/{id}/validate": { + "put": { + "tags": [ + "Document" + ], + "operationId": "PutUmbracoManagementApiV1.1DocumentByIdValidate1.1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel" + } + ] + } + }, + "text/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel" + } + ] + } + }, + "application/*+json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ValidateUpdateDocumentRequestModel" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "404": { + "description": "Not Found", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProblemDetails" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user do not have access to this resource", + "headers": { + "Umb-Notifications": { + "description": "The list of notifications produced during the request.", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NotificationHeaderModel" + }, + "nullable": true + } + } + } + } + }, "security": [ { "Backoffice User": [ ] @@ -45526,6 +45669,52 @@ }, "additionalProperties": false }, + "ValidateUpdateDocumentRequestModel": { + "required": [ + "values", + "variants" + ], + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentValueModel" + } + ] + } + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentVariantRequestModel" + } + ] + } + }, + "template": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ], + "nullable": true + }, + "cultures": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + } + }, + "additionalProperties": false + }, "VariantItemResponseModel": { "required": [ "name" diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs new file mode 100644 index 0000000000..806c733cc2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/ValidateUpdateDocumentRequestModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.Document; + +public class ValidateUpdateDocumentRequestModel : UpdateDocumentRequestModel +{ + public ISet? Cultures { get; set; } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/ValidateContentUpdateModel.cs b/src/Umbraco.Core/Models/ContentEditing/ValidateContentUpdateModel.cs new file mode 100644 index 0000000000..159398c3b1 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ValidateContentUpdateModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Core.Models.ContentEditing; + +public class ValidateContentUpdateModel : ContentUpdateModel +{ + public ISet? Cultures { get; set; } +} diff --git a/src/Umbraco.Core/Services/ContentEditingService.cs b/src/Umbraco.Core/Services/ContentEditingService.cs index 2ad8365bcf..a068f61b7a 100644 --- a/src/Umbraco.Core/Services/ContentEditingService.cs +++ b/src/Umbraco.Core/Services/ContentEditingService.cs @@ -36,6 +36,7 @@ internal sealed class ContentEditingService return await Task.FromResult(content); } + [Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")] public async Task> ValidateUpdateAsync(Guid key, ContentUpdateModel updateModel) { IContent? content = ContentService.GetById(key); @@ -44,8 +45,16 @@ internal sealed class ContentEditingService : Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); } + public async Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel) + { + IContent? content = ContentService.GetById(key); + return content is not null + ? await ValidateCulturesAndPropertiesAsync(updateModel, content.ContentType.Key, updateModel.Cultures) + : Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); + } + public async Task> ValidateCreateAsync(ContentCreateModel createModel) - => await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey); + => await ValidateCulturesAndPropertiesAsync(createModel, createModel.ContentTypeKey, createModel.Variants.Select(variant => variant.Culture)); public async Task> CreateAsync(ContentCreateModel createModel, Guid userKey) { @@ -137,10 +146,11 @@ internal sealed class ContentEditingService private async Task> ValidateCulturesAndPropertiesAsync( ContentEditingModelBase contentEditingModelBase, - Guid contentTypeKey) + Guid contentTypeKey, + IEnumerable? culturesToValidate = null) => await ValidateCulturesAsync(contentEditingModelBase) is false ? Attempt.FailWithStatus(ContentEditingOperationStatus.InvalidCulture, new ContentValidationResult()) - : await ValidatePropertiesAsync(contentEditingModelBase, contentTypeKey); + : await ValidatePropertiesAsync(contentEditingModelBase, contentTypeKey, culturesToValidate); private async Task UpdateTemplateAsync(IContent content, Guid? templateKey) { diff --git a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs index 290fc2e5d5..1f5c1dda9f 100644 --- a/src/Umbraco.Core/Services/ContentEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentEditingServiceBase.cs @@ -113,7 +113,8 @@ internal abstract class ContentEditingServiceBase> ValidatePropertiesAsync( ContentEditingModelBase contentEditingModelBase, - Guid contentTypeKey) + Guid contentTypeKey, + IEnumerable? culturesToValidate = null) { TContentType? contentType = await ContentTypeService.GetAsync(contentTypeKey); if (contentType is null) @@ -121,14 +122,15 @@ internal abstract class ContentEditingServiceBase> ValidatePropertiesAsync( ContentEditingModelBase contentEditingModelBase, - TContentType contentType) + TContentType contentType, + IEnumerable? culturesToValidate = null) { - ContentValidationResult result = await _validationService.ValidatePropertiesAsync(contentEditingModelBase, contentType); + ContentValidationResult result = await _validationService.ValidatePropertiesAsync(contentEditingModelBase, contentType, culturesToValidate); return result.ValidationErrors.Any() is false ? Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, result) : Attempt.FailWithStatus(ContentEditingOperationStatus.PropertyValidationError, result); diff --git a/src/Umbraco.Core/Services/ContentValidationService.cs b/src/Umbraco.Core/Services/ContentValidationService.cs index 093c9eaff3..0f93278c53 100644 --- a/src/Umbraco.Core/Services/ContentValidationService.cs +++ b/src/Umbraco.Core/Services/ContentValidationService.cs @@ -12,6 +12,7 @@ internal sealed class ContentValidationService : ContentValidationServiceBase ValidatePropertiesAsync( ContentEditingModelBase contentEditingModelBase, - IContentType contentType) - => await HandlePropertiesValidationAsync(contentEditingModelBase, contentType); + IContentType contentType, + IEnumerable? culturesToValidate = null) + => await HandlePropertiesValidationAsync(contentEditingModelBase, contentType, culturesToValidate); } diff --git a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs index b4cc9f2e51..63add24731 100644 --- a/src/Umbraco.Core/Services/ContentValidationServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentValidationServiceBase.cs @@ -23,7 +23,8 @@ internal abstract class ContentValidationServiceBase protected async Task HandlePropertiesValidationAsync( ContentEditingModelBase contentEditingModelBase, - TContentType contentType) + TContentType contentType, + IEnumerable? culturesToValidate = null) { var validationErrors = new List(); @@ -43,7 +44,7 @@ internal abstract class ContentValidationServiceBase return new ContentValidationResult { ValidationErrors = validationErrors }; } - var cultures = await GetCultureCodes(); + var cultures = culturesToValidate?.ToArray() ?? await GetCultureCodes(); // we don't have any managed segments, so we have to make do with the ones passed in the model var segments = contentEditingModelBase.Variants.DistinctBy(variant => variant.Segment).Select(variant => variant.Segment).ToArray(); diff --git a/src/Umbraco.Core/Services/IContentEditingService.cs b/src/Umbraco.Core/Services/IContentEditingService.cs index dc2fa92890..260e5ae934 100644 --- a/src/Umbraco.Core/Services/IContentEditingService.cs +++ b/src/Umbraco.Core/Services/IContentEditingService.cs @@ -10,8 +10,11 @@ public interface IContentEditingService Task> ValidateCreateAsync(ContentCreateModel createModel); + [Obsolete("Please use the validate update method that is not obsoleted. Will be removed in V16.")] Task> ValidateUpdateAsync(Guid key, ContentUpdateModel updateModel); + Task> ValidateUpdateAsync(Guid key, ValidateContentUpdateModel updateModel); + Task> CreateAsync(ContentCreateModel createModel, Guid userKey); Task> UpdateAsync(Guid key, ContentUpdateModel updateModel, Guid userKey); diff --git a/src/Umbraco.Core/Services/IContentValidationServiceBase.cs b/src/Umbraco.Core/Services/IContentValidationServiceBase.cs index 4218881766..7c91bdd0ec 100644 --- a/src/Umbraco.Core/Services/IContentValidationServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentValidationServiceBase.cs @@ -6,7 +6,7 @@ namespace Umbraco.Cms.Core.Services; internal interface IContentValidationServiceBase where TContentType : IContentTypeComposition { - Task ValidatePropertiesAsync(ContentEditingModelBase contentEditingModelBase, TContentType contentType); + Task ValidatePropertiesAsync(ContentEditingModelBase contentEditingModelBase, TContentType contentType, IEnumerable? culturesToValidate = null); Task ValidateCulturesAsync(ContentEditingModelBase contentEditingModelBase); } diff --git a/src/Umbraco.Core/Services/MediaValidationService.cs b/src/Umbraco.Core/Services/MediaValidationService.cs index 872dce7b28..0b46e61a87 100644 --- a/src/Umbraco.Core/Services/MediaValidationService.cs +++ b/src/Umbraco.Core/Services/MediaValidationService.cs @@ -12,6 +12,7 @@ internal sealed class MediaValidationService : ContentValidationServiceBase ValidatePropertiesAsync( ContentEditingModelBase contentEditingModelBase, - IMediaType mediaType) - => await HandlePropertiesValidationAsync(contentEditingModelBase, mediaType); + IMediaType mediaType, + IEnumerable? culturesToValidate = null) + => await HandlePropertiesValidationAsync(contentEditingModelBase, mediaType, culturesToValidate); } diff --git a/src/Umbraco.Core/Services/MemberValidationService.cs b/src/Umbraco.Core/Services/MemberValidationService.cs index 79ec4d75fc..db9e97587a 100644 --- a/src/Umbraco.Core/Services/MemberValidationService.cs +++ b/src/Umbraco.Core/Services/MemberValidationService.cs @@ -12,6 +12,7 @@ internal sealed class MemberValidationService : ContentValidationServiceBase ValidatePropertiesAsync( ContentEditingModelBase contentEditingModelBase, - IMemberType memberType) - => await HandlePropertiesValidationAsync(contentEditingModelBase, memberType); + IMemberType memberType, + IEnumerable? culturesToValidate = null) + => await HandlePropertiesValidationAsync(contentEditingModelBase, memberType, culturesToValidate); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs index 5ba83bafac..ef8d46a071 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs @@ -317,6 +317,93 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent Assert.AreEqual(expectedResult, result); } + [Test] + public async Task Can_Validate_For_All_Languages() + { + var contentType = await SetupLanguageTest(); + + var validationResult = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = [ + new() + { + Name = "Test Document (EN)", + Culture = "en-US", + Properties = [ + new() + { + Alias = "title", + Value = "Invalid value in English", + } + ] + }, + new() + { + Name = "Test Document (DA)", + Culture = "da-DK", + Properties = [ + new() + { + Alias = "title", + Value = "Invalid value in Danish", + } + ] + } + ] + }, + contentType); + + Assert.AreEqual(2, validationResult.ValidationErrors.Count()); + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "title" && r.Culture == "en-US" && r.JsonPath == string.Empty)); + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "title" && r.Culture == "da-DK" && r.JsonPath == string.Empty)); + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public async Task Can_Validate_For_Specific_Language(string culture) + { + var contentType = await SetupLanguageTest(); + + var validationResult = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = [ + new() + { + Name = "Test Document (EN)", + Culture = "en-US", + Properties = [ + new() + { + Alias = "title", + Value = "Invalid value in English", + } + ] + }, + new() + { + Name = "Test Document (DA)", + Culture = "da-DK", + Properties = [ + new() + { + Alias = "title", + Value = "Invalid value in Danish", + } + ] + } + ] + }, + contentType, + [culture]); + + Assert.AreEqual(1, validationResult.ValidationErrors.Count()); + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "title" && r.Culture == culture && r.JsonPath == string.Empty)); + } + private async Task<(IContentType DocumentType, IContentType ElementType)> SetupBlockListTest() { var propertyEditorCollection = GetRequiredService(); @@ -398,4 +485,22 @@ public class ContentValidationServiceTests : UmbracoIntegrationTestWithContent return contentType; } + + private async Task SetupLanguageTest() + { + var language = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(language, Constants.Security.SuperUserKey); + + var contentType = ContentTypeBuilder.CreateSimpleContentType("umbMandatory", "Mandatory Doc Type"); + contentType.Variations = ContentVariation.Culture; + var titlePropertyType = contentType.PropertyTypes.First(pt => pt.Alias == "title"); + titlePropertyType.Variations = ContentVariation.Culture; + titlePropertyType.ValidationRegExp = "^Valid.*$"; + contentType.AllowedAsRoot = true; + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + + return contentType; + } } From 6634822b8eadcac0d4a0efcff1592be6a86e4555 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 26 Sep 2024 09:07:24 +0200 Subject: [PATCH 34/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index 4729d3baa7..dfb8aefefe 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit 4729d3baa7611ed63380abcfc184c1bb5a48b3bb +Subproject commit dfb8aefefe5dd3268bac07d65fa1b09d4d189b01 From 5026095353fd7fff614de8584ade9c071922c226 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 26 Sep 2024 09:58:31 +0200 Subject: [PATCH 35/38] Post merge, to make built work --- Directory.Packages.props | 13 +++++++++---- .../Umbraco.Cms.Api.Delivery.csproj | 4 ++++ .../Umbraco.Cms.Persistence.EFCore.csproj | 11 ++++++++--- .../Umbraco.Cms.Persistence.SqlServer.csproj | 6 ++++++ .../Upgrade/V_14_0_0/AddGuidsToUserGroups.cs | 3 +++ .../Umbraco.Infrastructure.csproj | 3 +++ .../Umbraco.PublishedCache.HybridCache.csproj | 5 ++++- src/Umbraco.Web.Common/Umbraco.Web.Common.csproj | 2 ++ src/Umbraco.Web.Website/Umbraco.Web.Website.csproj | 4 ++++ version.json | 1 - 10 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 69633196fb..e60b2c718a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -47,7 +47,7 @@ - + @@ -75,7 +75,7 @@ - + @@ -88,6 +88,11 @@ - + + + + + + - + \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj index 5e32b81eeb..cc71f1e755 100644 --- a/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj +++ b/src/Umbraco.Cms.Api.Delivery/Umbraco.Cms.Api.Delivery.csproj @@ -12,6 +12,10 @@ + + + false + <_Parameter1>Umbraco.Tests.UnitTests diff --git a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj index d5fc7229b0..3c489dbb10 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj +++ b/src/Umbraco.Cms.Persistence.EFCore/Umbraco.Cms.Persistence.EFCore.csproj @@ -8,12 +8,17 @@ and remove this override --> IDE0270,CS0108,CS1998 - - - + + + + + + + + diff --git a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj index 1061c89526..52076f4562 100644 --- a/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj +++ b/src/Umbraco.Cms.Persistence.SqlServer/Umbraco.Cms.Persistence.SqlServer.csproj @@ -16,6 +16,12 @@ + + + + + + diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs index 67bdaf7395..461ed59c8e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_14_0_0/AddGuidsToUserGroups.cs @@ -23,6 +23,7 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase // If the new column already exists we'll do nothing. if (ColumnExists(Constants.DatabaseSchema.Tables.UserGroup, NewColumnName)) { + Context.Complete(); return; } @@ -31,10 +32,12 @@ public class AddGuidsToUserGroups : UnscopedMigrationBase if (DatabaseType != DatabaseType.SQLite) { MigrateSqlServer(); + Context.Complete(); return; } MigrateSqlite(); + Context.Complete(); } private void MigrateSqlServer() diff --git a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj index cca5a43954..ae2bb048d4 100644 --- a/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj +++ b/src/Umbraco.Infrastructure/Umbraco.Infrastructure.csproj @@ -39,6 +39,9 @@ + + + diff --git a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj index 6068233712..c37535f762 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj +++ b/src/Umbraco.PublishedCache.HybridCache/Umbraco.PublishedCache.HybridCache.csproj @@ -8,7 +8,10 @@ false - + + + false + diff --git a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj index 2da3121a5f..a99893accc 100644 --- a/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj +++ b/src/Umbraco.Web.Common/Umbraco.Web.Common.csproj @@ -26,6 +26,8 @@ + + diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj index 0e8d475725..76d800e74d 100644 --- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj +++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj @@ -12,6 +12,10 @@ and remove override --> ASP0019,CS0618,SA1401,SA1649,IDE0270,IDE1006 + + + false + diff --git a/version.json b/version.json index a155d77245..36cd9614b6 100644 --- a/version.json +++ b/version.json @@ -1,7 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", "version": "15.0.0-rc1", - "version": "14.4.0-rc", "assemblyVersion": { "precision": "build" }, From cf3b9d60f0c8d35656fe238e377295ee7fce1a57 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 26 Sep 2024 11:01:12 +0200 Subject: [PATCH 36/38] Disable package validation for v15 again --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index d48bbeeb74..0df4f75e8a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,7 +30,7 @@ false - true + false 14.0.0 true true From e624e2d8a5fc61e8d325d1ba8eb596c3e71bad88 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:41:29 +0200 Subject: [PATCH 37/38] update backoffice submodule --- src/Umbraco.Web.UI.Client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client index dfb8aefefe..e2782af371 160000 --- a/src/Umbraco.Web.UI.Client +++ b/src/Umbraco.Web.UI.Client @@ -1 +1 @@ -Subproject commit dfb8aefefe5dd3268bac07d65fa1b09d4d189b01 +Subproject commit e2782af3719ea1715e3f995fc9b48e04ce63774f From 3180ab3ed09b9401dd9e1be88bce61312b6380d7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 27 Sep 2024 08:23:20 +0200 Subject: [PATCH 38/38] Temp disable windows integration tests, due to lack of memory on the free instances (#17147) --- build/azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index 9e7207bf07..b7fadde6d5 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -287,8 +287,8 @@ stages: displayName: Integration Tests (SQLite) strategy: matrix: - Windows: - vmImage: 'windows-latest' +# Windows: +# vmImage: 'windows-latest' Linux: vmImage: 'ubuntu-latest' macOS: