From e0cdd3bfc1540ac822ec3bbd2b5600c1a4f39f8e Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 25 Apr 2022 18:40:30 +0200 Subject: [PATCH] Upsert patch. (#872) --- .../FieldDescriptions.Designer.cs | 9 ++ .../FieldDescriptions.resx | 3 + .../Contents/Commands/BulkUpdateJob.cs | 2 + .../Contents/Commands/UpsertContent.cs | 2 + .../DomainObject/ContentDomainObject.cs | 10 +- .../GraphQL/Types/Contents/ContentActions.cs | 9 +- .../Generator/SchemasOpenApiGenerator.cs | 16 +-- .../Models/BulkUpdateContentsJobDto.cs | 5 + .../Contents/Models/UpsertContentDto.cs | 15 +-- .../DomainObject/ContentDomainObjectTests.cs | 22 ++++ .../TestSuite.ApiTests/ContentUpdateTests.cs | 100 ++++++++++++++++++ .../TestSuite.Shared/TestSuite.Shared.csproj | 2 +- 12 files changed, 177 insertions(+), 18 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs index b87d6169e..3fae399d5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs @@ -492,6 +492,15 @@ namespace Squidex.Domain.Apps.Core { } } + /// + /// Looks up a localized string similar to Makes the update as patch.. + /// + public static string ContentRequestPatch { + get { + return ResourceManager.GetString("ContentRequestPatch", resourceCulture); + } + } + /// /// Looks up a localized string similar to Set to true to autopublish content on create.. /// diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx index 783b3a224..9698e8835 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx @@ -261,6 +261,9 @@ The initial status. + + Makes the update as patch. + Set to true to autopublish content on create. diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs index 1e9f621fd..b52748301 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/BulkUpdateJob.cs @@ -29,6 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public string? Schema { get; set; } + public bool Patch { get; set; } + public bool Permanent { get; set; } public long ExpectedCount { get; set; } = 1; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs index 5db1f76d0..b6e3637ec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpsertContent.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands public bool CheckReferrers { get; set; } + public bool Patch { get; set; } + public UpsertContent() { ContentId = DomainId.NewGuid(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 3f657aeb4..cb69bee00 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -72,13 +72,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); - if (Version > EtagVersion.Empty && !IsDeleted(Snapshot)) + if (Version <= EtagVersion.Empty || IsDeleted(Snapshot)) { - await UpdateCore(c.AsUpdate(), operation); + await CreateCore(c.AsCreate(), operation); + } + else if (c.Patch) + { + await PatchCore(c.AsUpdate(), operation); } else { - await CreateCore(c.AsCreate(), operation); + await UpdateCore(c.AsUpdate(), operation); } if (Is.OptionalChange(operation.Snapshot.EditingStatus(), c.Status)) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index e8a18bd17..1b54fa289 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -277,6 +277,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents Description = FieldDescriptions.ContentRequestPublish, DefaultValue = false }, + new QueryArgument(AllTypes.Boolean) + { + Name = "patch", + Description = FieldDescriptions.ContentRequestPatch, + DefaultValue = false + }, new QueryArgument(AllTypes.String) { Name = "status", @@ -297,8 +303,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents var contentId = c.GetArgument("id"); var contentData = c.GetArgument("data")!; var contentStatus = c.GetArgument("status"); + var patch = c.GetArgument("patch"); - var command = new UpsertContent { ContentId = contentId, Data = contentData }; + var command = new UpsertContent { ContentId = contentId, Data = contentData, Patch = patch }; if (!string.IsNullOrWhiteSpace(contentStatus)) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs index f788eace3..e8cd15cf3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemasOpenApiGenerator.cs @@ -9,13 +9,14 @@ using NJsonSchema; using NSwag; using NSwag.Generation; using NSwag.Generation.Processors.Contexts; +using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Hosting; using Squidex.Infrastructure.Caching; using Squidex.Properties; using Squidex.Shared; +using IRequestUrlGenerator = Squidex.Hosting.IUrlGenerator; using SchemaDefType = Squidex.Domain.Apps.Core.Schemas.SchemaType; namespace Squidex.Areas.Api.Controllers.Contents.Generator @@ -23,16 +24,16 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator public sealed class SchemasOpenApiGenerator { private readonly IAppProvider appProvider; - private readonly IUrlGenerator urlGenerator; private readonly OpenApiDocumentGeneratorSettings schemaSettings; private readonly OpenApiSchemaGenerator schemaGenerator; + private readonly IRequestUrlGenerator urlGenerator; private readonly IRequestCache requestCache; public SchemasOpenApiGenerator( IAppProvider appProvider, - IUrlGenerator urlGenerator, OpenApiDocumentGeneratorSettings schemaSettings, OpenApiSchemaGenerator schemaGenerator, + IRequestUrlGenerator urlGenerator, IRequestCache requestCache) { this.appProvider = appProvider; @@ -123,7 +124,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator .RequirePermission(Permissions.AppContentsReadOwn) .Operation("GetVersioned") .OperationSummary("Get a [schema] content item by id and version.") - .HasPath("version", JsonObjectType.Number, "The version of the content item.") + .HasPath("version", JsonObjectType.Number, FieldDescriptions.EntityVersion) .HasId() .Responds(200, "Content item returned.", builder.ContentSchema); @@ -139,8 +140,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator .RequirePermission(Permissions.AppContentsCreate) .Operation("Create") .OperationSummary("Create a [schema] content item.") - .HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.") - .HasQuery("id", JsonObjectType.String, "The optional custom content id.") + .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) + .HasQuery("id", JsonObjectType.String, FieldDescriptions.ContentRequestOptionalId) .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .Responds(201, "Content item created", builder.ContentSchema) .Responds(400, "Content data not valid."); @@ -149,7 +150,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator .RequirePermission(Permissions.AppContentsUpsert) .Operation("Upsert") .OperationSummary("Upsert a [schema] content item.") - .HasQuery("publish", JsonObjectType.Boolean, "True to automatically publish the content.") + .HasQuery("patch", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPatch) + .HasQuery("publish", JsonObjectType.Boolean, FieldDescriptions.ContentRequestPublish) .HasId() .HasBody("data", builder.DataSchema, Resources.OpenApiSchemaBody) .Responds(200, "Content item created or updated.", builder.ContentSchema) diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs index 93ff2716b..991a3e9b2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/BulkUpdateContentsJobDto.cs @@ -50,6 +50,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public string? Schema { get; set; } + /// + /// Makes the update as patch. + /// + public bool Patch { get; set; } + /// /// True to delete the content permanently. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs index 5926507e0..c4eed4cdf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/UpsertContentDto.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Infrastructure; +using Squidex.Infrastructure.Reflection; using StatusType = Squidex.Domain.Apps.Core.Contents.Status; namespace Squidex.Areas.Api.Controllers.Contents.Models @@ -27,6 +28,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models [FromQuery] public StatusType? Status { get; set; } + /// + /// Makes the update as patch. + /// + [FromQuery] + public bool Patch { get; set; } + /// /// True to automatically publish the content. /// @@ -36,14 +43,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public UpsertContent ToCommand(DomainId id) { - var command = new UpsertContent { Data = Data!, ContentId = id }; + var command = SimpleMapper.Map(this, new UpsertContent { ContentId = id }); - if (Status != null) - { - command.Status = Status; - } #pragma warning disable CS0618 // Type or member is obsolete - else if (Publish) + if (command.Status == null && Publish) { command.Status = StatusType.Published; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs index 401346096..24cacb6a3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs @@ -325,6 +325,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject .MustHaveHappened(); } + [Fact] + public async Task Upsert_should_patch_content_if_found() + { + var command = new UpsertContent { Data = patch, Patch = true }; + + await ExecuteCreateAsync(); + + var result = await PublishAsync(CreateContentCommand(command)); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.Equal(patched, sut.Snapshot.CurrentVersion.Data); + + LastEvents + .ShouldHaveSameEvents( + CreateContentEvent(new ContentUpdated { Data = patched }) + ); + + A.CallTo(() => scriptEngine.TransformAsync(DataScriptVars(patched, data, Status.Draft), "", ScriptOptions(), default)) + .MustHaveHappened(); + } + [Fact] public async Task Upsert_should_not_change_status_on_update_if_status_set_to_initial() { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 43f2b81c2..32e810709 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -465,6 +465,106 @@ namespace TestSuite.ApiTests } } + [Fact] + public async Task Should_patch_content_with_upsert() + { + TestEntity content = null; + try + { + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { String = "test" }, ContentCreateOptions.AsPublish); + + + // STEP 2: Path an item. + await _.Contents.UpsertAsync(content.Id, new TestEntityData { Number = 1 }, ContentUpsertOptions.AsPatch); + + + // STEP 3: Update the item and ensure that the data has changed. + await _.Contents.UpsertAsync(content.Id, new TestEntityData { Number = 2 }, ContentUpsertOptions.AsPatch); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, updated.Data.Number); + + // Should not change other value with patch. + Assert.Equal("test", updated.Data.String); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_patch_content_with_bulk() + { + TestEntity content = null; + try + { + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { String = "test" }, ContentCreateOptions.AsPublish); + + + // STEP 2: Path an item. + await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List + { + new BulkUpdateJob + { + Id = content.Id, + Data = new + { + number = new + { + iv = 1 + } + }, + Patch = true + } + } + }); + + + // STEP 3: Update the item and ensure that the data has changed. + await _.Contents.BulkUpdateAsync(new BulkUpdate + { + Jobs = new List + { + new BulkUpdateJob + { + Id = content.Id, + Data = new + { + number = new + { + iv = 2 + } + }, + Patch = true + } + } + }); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, updated.Data.Number); + + // Should not change other value with patch. + Assert.Equal("test", updated.Data.String); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + [Fact] public async Task Should_create_draft_version() { diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index bd203d027..7d3009b94 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -16,7 +16,7 @@ - +