diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index e7cbe2c56..b9a9e3f9c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -23,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { public sealed class ContentQueryService : IContentQueryService { + private const string SingletonId = "_schemaId_"; private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); private readonly IAppProvider appProvider; private readonly IContentEnricher contentEnricher; @@ -58,6 +59,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries IContentEntity? content; + if (id.ToString().Equals(SingletonId)) + { + id = schema.Id; + } + if (version > EtagVersion.Empty) { content = await contentLoader.GetAsync(context.App.Id, id, version); diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs new file mode 100644 index 000000000..9c1c1b240 --- /dev/null +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Web.CommandMiddlewares +{ + public sealed class EnrichWithContentIdCommandMiddleware : ICommandMiddleware + { + private const string SingletonId = "_schemaId_"; + + public Task HandleAsync(CommandContext context, NextDelegate next) + { + if (context.Command is ContentCommand contentCommand && contentCommand is not CreateContent) + { + if (contentCommand.ContentId.ToString().Equals(SingletonId)) + { + contentCommand.ContentId = contentCommand.SchemaId.Id; + } + } + + return next(context); + } + } +} diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index ea42029db..195c55b89 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -55,6 +55,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 9b6558391..9e75ef113 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -137,6 +137,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries Assert.Null(await sut.FindAsync(requestContext, schemaId.Name, content.Id)); } + [Fact] + public async Task Should_return_content_by_special_id() + { + var requestContext = CreateContext(); + + var content = CreateContent(DomainId.NewGuid()); + + A.CallTo(() => contentRepository.FindContentAsync(requestContext.App, schema, schema.Id, SearchScope.Published, A._)) + .Returns(content); + + var result = await sut.FindAsync(requestContext, schemaId.Name, DomainId.Create("_schemaId_")); + + AssertContent(content, result); + } + [Theory] [InlineData(1, 0, SearchScope.All)] [InlineData(1, 1, SearchScope.All)] diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs new file mode 100644 index 000000000..553db6a1e --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using FakeItEasy; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Web.CommandMiddlewares +{ + public class EnrichWithContentIdCommandMiddlewareTests + { + private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); + private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); + private readonly EnrichWithContentIdCommandMiddleware sut; + + public EnrichWithContentIdCommandMiddlewareTests() + { + sut = new EnrichWithContentIdCommandMiddleware(); + } + + [Fact] + public async Task Should_replace_content_id_with_schema_id_if_placeholder_used() + { + var command = new UpdateContent + { + ContentId = DomainId.Create("_schemaId_") + }; + + await HandleAsync(command); + + Assert.Equal(schemaId.Id, command.ContentId); + } + + [Fact] + public async Task Should_not_replace_content_id_with_schema_for_create_command() + { + var command = new CreateContent + { + ContentId = DomainId.Create("_schemaId_") + }; + + await HandleAsync(command); + + Assert.NotEqual(schemaId.Id, command.ContentId); + } + + [Fact] + public async Task Should_not_replace_content_id_with_schema_id_if_placeholder_not_used() + { + var command = new UpdateContent + { + ContentId = DomainId.Create("{custom}") + }; + + await HandleAsync(command); + + Assert.NotEqual(schemaId.Id, command.ContentId); + } + + private async Task HandleAsync(ContentCommand command) + { + command.AppId = appId; + command.SchemaId = schemaId; + + var commandContext = new CommandContext(command, A.Fake()); + + await sut.HandleAsync(commandContext); + + return commandContext; + } + } +} diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs index 7b1616f91..44fe1d3d9 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs @@ -29,7 +29,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_cleanup_old_data_from_update_response() { - var schemaName = $"schema-{DateTime.UtcNow.Ticks}"; + var schemaName = $"schema-{Guid.NewGuid()}"; // STEP 1: Create a schema. var schema = await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); @@ -59,7 +59,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_cleanup_old_references() { - var schemaName = $"schema-{DateTime.UtcNow.Ticks}"; + var schemaName = $"schema-{Guid.NewGuid()}"; // STEP 1: Create a schema. await TestEntityWithReferences.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs index 4fc29cb94..d27ecdf6b 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs @@ -29,7 +29,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_use_creating_and_query_tests() { - var schemaName = $"schema-{DateTime.UtcNow.Ticks}"; + var schemaName = $"schema-{Guid.NewGuid()}"; // STEP 1: Create a schema. await TestEntity.CreateSchemaAsync(_.Schemas, _.AppName, schemaName); diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index aa228a480..a85432318 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -9,7 +9,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; using TestSuite.Fixtures; using TestSuite.Model; using Xunit; @@ -646,5 +648,49 @@ namespace TestSuite.ApiTests Assert.NotNull(contents_4.Items.FirstOrDefault(x => x.Id == content_1.Id)); } + + [Fact] + public async Task Should_update_singleton_content_with_special_id() + { + var schemaName = $"schema-{Guid.NewGuid()}"; + + // STEP 1: Create singleton. + var createRequest = new CreateSchemaDto + { + Name = schemaName, + IsPublished = true, + IsSingleton = true, + Fields = new List + { + new UpsertSchemaFieldDto + { + Name = "my-field", + Properties = new StringFieldPropertiesDto() + } + } + }; + + await _.Schemas.PostSchemaAsync(_.AppName, createRequest); + + + var client = _.ClientManager.CreateDynamicContentsClient(schemaName); + + // STEP 2: Get content. + var content_1 = await client.GetAsync("_schemaId_"); + + Assert.NotNull(content_1); + + + // STEP 3: Update content. + var content_2 = await client.UpdateAsync("_schemaId_", new DynamicData + { + ["my-field"] = new JObject + { + ["iv"] = "singleton" + } + }); + + Assert.Equal("singleton", content_2.Data["my-field"]["iv"]); + } } }