From f426371aac68092ff0b8cec592532e1cbd623422 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 22 Jun 2025 21:55:12 +0200 Subject: [PATCH] X fields (#1233) * Small UI improvements. * Support X-Fields for other endpoints. * Fix tests --- .../Entities/Contents/EFContentRepository.cs | 13 +- .../Contents/MongoContentCollection.cs | 4 +- .../Contents/MongoContentRepository.cs | 4 +- .../Contents/MongoShardedContentRepository.cs | 4 +- .../Entities/Contents/Operations/QueryById.cs | 4 +- .../ConvertContent/AddDefaultValues.cs | 2 +- .../ConvertContent/ExcludeOtherFields.cs | 19 + .../Scripting/ScriptingCompleter.cs | 3 +- .../Guards/ValidationExtensions.cs | 2 +- .../Contents/Queries/ContentQueryService.cs | 9 +- .../Contents/Queries/Steps/ConvertData.cs | 5 + .../Repositories/IContentRepository.cs | 2 +- .../Shared/ContentRepositoryTests.cs | 46 ++ .../ConvertContent/FieldConvertersTests.cs | 35 + .../Scripting/ScriptingCompleterTests.cs | 15 +- .../ValidationTestExtensions.cs | 2 +- .../Queries/ContentQueryServiceTests.cs | 24 +- .../TestSuite.ApiTests/AssetTests.cs | 74 +- .../TestSuite.ApiTests/BackupTests.cs | 9 +- .../TestSuite.ApiTests/ContentCleanupTests.cs | 46 +- .../ContentCollationTests.cs | 34 +- .../ContentLanguageTests.cs | 83 +- .../TestSuite.ApiTests/ContentQueryTests.cs | 44 +- .../ContentReferencesTests.cs | 245 +++--- .../ContentScriptingTests.cs | 99 +-- .../TestSuite.ApiTests/ContentUpdateTests.cs | 741 ++++++++++-------- .../TestSuite.ApiTests/GraphQLTests.cs | 20 +- .../TestSuite.ApiTests/RuleRunnerTests.cs | 45 +- 28 files changed, 980 insertions(+), 653 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeOtherFields.cs diff --git a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs index 68e949051..596399569 100644 --- a/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs +++ b/backend/src/Squidex.Data.EntityFramework/Domain/Apps/Entities/Contents/EFContentRepository.cs @@ -27,18 +27,18 @@ public sealed partial class EFContentRepository( { private readonly DynamicTables dynamicTables = new DynamicTables(dbContextFactory, dbContentContextFactory); - public async Task FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope, + public async Task FindContentAsync(App app, Schema schema, DomainId id, IReadOnlySet? fields, SearchScope scope, CancellationToken ct = default) { using (Telemetry.Activities.StartActivity("EFContentRepository/FindContentAsync")) { return scope == SearchScope.All ? - await FindContentAsync(app.Id, schema.Id, id, ct) : - await FindContentAsync(app.Id, schema.Id, id, ct); + await FindContentAsync(app.Id, schema.Id, id, fields, ct) : + await FindContentAsync(app.Id, schema.Id, id, fields, ct); } } - public async Task FindContentAsync(DomainId appId, DomainId schemaId, DomainId id, + public async Task FindContentAsync(DomainId appId, DomainId schemaId, DomainId id, IReadOnlySet? fields, CancellationToken ct = default) where T : EFContentEntity { await using var dbContext = await CreateDbContextAsync(ct); @@ -49,6 +49,11 @@ public sealed partial class EFContentRepository( .Where(x => x.IndexedSchemaId == schemaId) .FirstOrDefaultAsync(ct); + if (fields?.Count > 0) + { + entity?.Data.LimitFields(fields); + } + return entity; } diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs index 0f2f7bf31..b80ca0fc1 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentCollection.cs @@ -213,12 +213,12 @@ public sealed class MongoContentCollection : MongoRepositoryBase FindContentAsync(Schema schema, DomainId id, + public async Task FindContentAsync(Schema schema, DomainId id, IReadOnlySet? fields, CancellationToken ct) { using (Telemetry.Activities.StartActivity("MongoContentCollection/FindContentAsync")) { - return await queryBdId.QueryAsync(schema, id, ct); + return await queryBdId.QueryAsync(schema, id, fields, ct); } } diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository.cs index 7435e9235..954dec61c 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoContentRepository.cs @@ -106,10 +106,10 @@ public partial class MongoContentRepository( return GetCollection(scope).QueryAsync(app, schema, q, ct); } - public Task FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope, + public Task FindContentAsync(App app, Schema schema, DomainId id, IReadOnlySet? fields, SearchScope scope, CancellationToken ct = default) { - return GetCollection(scope).FindContentAsync(schema, id, ct); + return GetCollection(scope).FindContentAsync(schema, id, fields, ct); } public Task> QueryIdsAsync(App app, HashSet ids, SearchScope scope, diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoShardedContentRepository.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoShardedContentRepository.cs index 73b20253d..bba70bfe9 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoShardedContentRepository.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/MongoShardedContentRepository.cs @@ -21,10 +21,10 @@ namespace Squidex.Domain.Apps.Entities.Contents; public sealed class MongoShardedContentRepository(IShardingStrategy sharding, Func factory) : ShardedSnapshotStore(sharding, factory, x => x.AppId.Id), IContentRepository, IDeleter { - public Task FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope, + public Task FindContentAsync(App app, Schema schema, DomainId id, IReadOnlySet? fields, SearchScope scope, CancellationToken ct = default) { - return Shard(app.Id).FindContentAsync(app, schema, id, scope, ct); + return Shard(app.Id).FindContentAsync(app, schema, id, fields, scope, ct); } public Task HasReferrersAsync(App app, DomainId reference, SearchScope scope, diff --git a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryById.cs b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryById.cs index abbe83f23..08e16a70a 100644 --- a/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryById.cs +++ b/backend/src/Squidex.Data.MongoDb/Domain/Apps/Entities/Contents/Operations/QueryById.cs @@ -14,12 +14,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Operations; internal sealed class QueryById : OperationBase { - public async Task QueryAsync(Schema schema, DomainId id, + public async Task QueryAsync(Schema schema, DomainId id, IReadOnlySet? fields, CancellationToken ct) { var filter = Filter.Eq(x => x.DocumentId, DomainId.Combine(schema.AppId, id)); - var contentEntity = await Collection.Find(filter).SelectFields(null).FirstOrDefaultAsync(ct); + var contentEntity = await Collection.Find(filter).SelectFields(fields).FirstOrDefaultAsync(ct); if (contentEntity == null || contentEntity.IndexedSchemaId != schema.Id) { return null; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddDefaultValues.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddDefaultValues.cs index ac49d06b4..6768cdd85 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddDefaultValues.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/AddDefaultValues.cs @@ -82,7 +82,7 @@ public sealed class AddDefaultValues(PartitionResolver partitionResolver, IClock private void Enrich(IField field, Dictionary fieldData, string key) { - if (fieldData.TryGetValue(key, out _) || (field.RawProperties.IsRequired && IgnoreRequiredFields)) + if (fieldData.ContainsKey(key) || (field.RawProperties.IsRequired && IgnoreRequiredFields)) { return; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeOtherFields.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeOtherFields.cs new file mode 100644 index 000000000..579ec7358 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeOtherFields.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Core.ConvertContent; + +public sealed class ExcludeOtherFields(HashSet fieldsToInclude) : IContentFieldConverter +{ + public ContentFieldData? ConvertFieldBefore(IRootField field, ContentFieldData source) + { + return fieldsToInclude.Contains(field.Name) ? source : null; + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs index 11457179a..0b4d6b57a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ScriptingCompleter.cs @@ -233,7 +233,6 @@ public sealed partial class ScriptingCompleter(IEnumerable de AddObject("ctx", FieldDescriptions.FieldRuleContext, () => { - Add(JsonType.String, "action", FieldDescriptions.UserAppRole, ["Create", "Update"]); }); @@ -251,7 +250,7 @@ public sealed partial class ScriptingCompleter(IEnumerable de return Build(); } - private IReadOnlyList Build() + private List Build() { return result.Values.OrderBy(x => x.Path).ToList(); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs index 57302b273..fc3b8160f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ValidationExtensions.cs @@ -150,7 +150,7 @@ public static class ValidationExtensions operation.Components, operation.Resolve()) { - PreviousData = previousData + PreviousData = previousData, }; var validationContext = new ValidationContext(rootContext).Optimized(optimize).AsPublishing(published); 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 39f60bd20..3b1bbc56f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -190,12 +190,7 @@ public sealed class ContentQueryService( { var schema = await GetSchemaAsync(context, schemaIdOrName, ct); - if (schema == null) - { - throw new DomainObjectNotFoundException(schemaIdOrName); - } - - return schema; + return schema ?? throw new DomainObjectNotFoundException(schemaIdOrName); } public async Task GetSchemaAsync(Context context, string schemaIdOrName, @@ -277,7 +272,7 @@ public sealed class ContentQueryService( // Enforce a hard timeout combined.CancelAfter(options.TimeoutFind); - return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token); + return await contentRepository.FindContentAsync(context.App, schema, id, context.Fields(), context.Scope(), combined.Token); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index 794f4e005..bddba7659 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -154,6 +154,11 @@ public sealed class ConvertData( } } + if (fieldNames?.Count > 0) + { + converter.Add(new ExcludeOtherFields(fieldNames)); + } + if (!context.IsFrontendClient || context.ResolveSchemaNames()) { converter.Add(new AddSchemaNames(components)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index cf2735336..213922ad5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -41,7 +41,7 @@ public interface IContentRepository Task> QueryIdsAsync(App app, HashSet ids, SearchScope scope, CancellationToken ct = default); - Task FindContentAsync(App app, Schema schema, DomainId id, SearchScope scope, + Task FindContentAsync(App app, Schema schema, DomainId id, IReadOnlySet? fields, SearchScope scope, CancellationToken ct = default); Task HasReferrersAsync(App app, DomainId reference, SearchScope scope, diff --git a/backend/tests/Squidex.Data.Tests/Shared/ContentRepositoryTests.cs b/backend/tests/Squidex.Data.Tests/Shared/ContentRepositoryTests.cs index a18b19dec..f09436856 100644 --- a/backend/tests/Squidex.Data.Tests/Shared/ContentRepositoryTests.cs +++ b/backend/tests/Squidex.Data.Tests/Shared/ContentRepositoryTests.cs @@ -163,6 +163,32 @@ public abstract class ContentRepositoryTests : GivenContext return sut; } + [Fact] + public async Task Should_find_by_id() + { + var sut = await CreateAndPrepareSutAsync(); + + var contentId = await sut.StreamAll(app.Id, [schema.Id], default).Select(x => x.Id).FirstOrDefaultAsync(); + var content = await sut.FindContentAsync(app, schema, contentId, null, SearchScope.All); + + // ID is not predicable, therefore the weak assertion. + Assert.NotNull(content); + } + + [Fact] + public async Task Should_find_by_id_with_limited_fields() + { + var sut = await CreateAndPrepareSutAsync(); + + var contentId = await sut.StreamAll(app.Id, [schema.Id], default).Select(x => x.Id).FirstOrDefaultAsync(); + var content = await sut.FindContentAsync(app, schema, contentId, HashSet.Of("field1"), SearchScope.All); + + // Only check that the we only go one field. + Assert.NotNull(content); + Assert.Single(content.Data); + Assert.Contains("field1", content.Data); + } + [Fact] public async Task Should_stream_all_with_schema() { @@ -170,6 +196,7 @@ public abstract class ContentRepositoryTests : GivenContext var count = await sut.StreamAll(AppIds[0].Id, [schema.Id], SearchScope.All).CountAsync(); + // IDs is not predicable, therefore the weak assertion. Assert.Equal(NumValues, count); } @@ -180,6 +207,7 @@ public abstract class ContentRepositoryTests : GivenContext var count = await sut.StreamAll(AppIds[0].Id, null, SearchScope.All).CountAsync(); + // IDs is not predicable, therefore the weak assertion. Assert.Equal(NumValues * SchemaIds.Length, count); } @@ -190,6 +218,7 @@ public abstract class ContentRepositoryTests : GivenContext var count = await sut.StreamAll(AppIds[0].Id, [], SearchScope.All).CountAsync(); + // IDs is not predicable, therefore the weak assertion. Assert.Equal(0, count); } @@ -288,6 +317,21 @@ public abstract class ContentRepositoryTests : GivenContext Assert.Equal(NumValues, contents.Count); } + [Fact] + public async Task Should_query_with_fields() + { + var query = new ClrQuery(); + + var contents = await QueryAsync(query, fields: HashSet.Of("field1")); + + // We have a concrete query, so we expect an actual result. + Assert.All(contents, content => + { + Assert.Single(content.Data); + Assert.Contains("field1", content.Data); + }); + } + [Fact] public async Task Should_query_with_large_skip() { @@ -378,6 +422,7 @@ public abstract class ContentRepositoryTests : GivenContext int skip = 0, DomainId reference = default, DomainId referencing = default, + HashSet? fields = null, bool withTotal = false) { clrQuery.Take = top; @@ -396,6 +441,7 @@ public abstract class ContentRepositoryTests : GivenContext var q = Q.Empty + .WithFields(fields) .WithoutTotal(!withTotal) .WithQuery(clrQuery) .WithReference(reference) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index 3c6961dc6..dd7743d1b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -128,6 +128,41 @@ public class FieldConvertersTests Assert.Equal(expected, actual); } + [Fact] + public void Should_remove_non_included_fields() + { + var field1 = Fields.Number(1, "number1", Partitioning.Language); + var field2 = Fields.Number(2, "number2", Partitioning.Language); + + var schema = + new Schema { Name = "my-schema" } + .AddField(field1) + .AddField(field2); + + var source = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)) + .AddField(field2.Name, + new ContentFieldData() + .AddLocalized("en", JsonValue.Null) + .AddLocalized("de", 1)); + + var actual = + new ContentConverter(ResolvedComponents.Empty, schema) + .Add(new ExcludeOtherFields(HashSet.Of(field1.Name))) + .Convert(source); + + var expected = + new ContentData() + .AddField(field1.Name, + new ContentFieldData() + .AddLocalized("en", 1)); + + Assert.Equal(expected, actual); + } + [Fact] public void Should_not_remove_hidden_fields() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index 1ab02f642..37e6a0712 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -177,8 +177,7 @@ public class ScriptingCompleterTests var actual = sut.FieldRule(dataSchema); AssertCompletion(actual, - new[] - { + [ "ctx", "ctx.action", "data", @@ -191,7 +190,7 @@ public class ScriptingCompleterTests "user.email", "user.id", "user.role", - }); + ]); } [Fact] @@ -200,15 +199,14 @@ public class ScriptingCompleterTests var actual = sut.PreviewUrl(dataSchema); AssertCompletion(actual, - new[] - { + [ "accessToken", "data", "data['my-field']", "data['my-field'].iv", "id", "version", - }); + ]); } [Fact] @@ -356,8 +354,7 @@ public class ScriptingCompleterTests private static void AssertUsageTrigger(IReadOnlyList actual) { AssertCompletion(actual, - new[] - { + [ "event", "event.appId", "event.appId.id", @@ -367,7 +364,7 @@ public class ScriptingCompleterTests "event.name", "event.timestamp", "event.version", - }); + ]); } private static void AssertCompletion(IReadOnlyList actual, params string[][] expected) diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index a99ec9ab0..c9ab13b7b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -142,7 +142,7 @@ public static class ValidationTestExtensions components ?? ResolvedComponents.Empty, TestUtils.DefaultSerializer) { - PreviousData = previousData + PreviousData = previousData, }; var context = 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 786955991..d37bb6664 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 @@ -22,7 +22,6 @@ public class ContentQueryServiceTests : GivenContext private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContentRepository contentRepository = A.Fake(); private readonly IContentLoader contentVersionLoader = A.Fake(); - private readonly ContentData contentData = []; private readonly ContentQueryParser queryParser = A.Fake(); private readonly ContentQueryService sut; @@ -59,11 +58,9 @@ public class ContentQueryServiceTests : GivenContext [Fact] public async Task Should_get_schema_from_guid_string() { - var input = SchemaId.Id.ToString(); - var requestContext = SetupContext(); - var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, CancellationToken); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, SchemaId.Id.ToString(), CancellationToken); Assert.Equal(Schema, actual); } @@ -71,11 +68,9 @@ public class ContentQueryServiceTests : GivenContext [Fact] public async Task Should_get_schema_from_name() { - var input = SchemaId.Name; - var requestContext = SetupContext(); - var actual = await sut.GetSchemaOrThrowAsync(requestContext, input, CancellationToken); + var actual = await sut.GetSchemaOrThrowAsync(requestContext, SchemaId.Name, CancellationToken); Assert.Equal(Schema, actual); } @@ -96,21 +91,19 @@ public class ContentQueryServiceTests : GivenContext var requestContext = SetupContext(allowSchema: false); var content = CreateContent() as Content; - - A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, A._, A._)) + A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, null, A._, A._)) .Returns(content); await Assert.ThrowsAsync(() => sut.FindAsync(requestContext, SchemaId.Name, content.Id, ct: CancellationToken)); } [Fact] - public async Task Should_return_null_if_content_by_id_dannot_be_found() + public async Task Should_return_null_if_content_by_id_cannot_be_found() { var requestContext = SetupContext(); var content = CreateContent(); - - A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, A._, A._)) + A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, A>._, A._, A._)) .Returns(null); var actual = await sut.FindAsync(requestContext, SchemaId.Name, content.Id, ct: CancellationToken); @@ -124,8 +117,7 @@ public class ContentQueryServiceTests : GivenContext var requestContext = SetupContext(); var content = CreateContent(); - - A.CallTo(() => contentRepository.FindContentAsync(App, Schema, SchemaId.Id, SearchScope.Published, A._)) + A.CallTo(() => contentRepository.FindContentAsync(App, Schema, SchemaId.Id, A>._, SearchScope.Published, A._)) .Returns(content); var actual = await sut.FindAsync(requestContext, SchemaId.Name, DomainId.Create("_schemaId_"), ct: CancellationToken); @@ -143,8 +135,7 @@ public class ContentQueryServiceTests : GivenContext var requestContext = SetupContext(isFrontend, isUnpublished: unpublished); var content = CreateContent(); - - A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, scope, A._)) + A.CallTo(() => contentRepository.FindContentAsync(App, Schema, content.Id, A>._, scope, A._)) .Returns(content); var actual = await sut.FindAsync(requestContext, SchemaId.Name, content.Id, ct: CancellationToken); @@ -158,7 +149,6 @@ public class ContentQueryServiceTests : GivenContext var requestContext = SetupContext(); var content = CreateContent(); - A.CallTo(() => contentVersionLoader.GetAsync(AppId.Id, content.Id, 13, A._)) .Returns(content); diff --git a/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index 15d012537..1f1d17d03 100644 --- a/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -589,10 +589,11 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture x.Id == asset_1.Id); @@ -600,17 +601,20 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture x.Id == asset_1.Id); } @@ -623,10 +627,11 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture x.Id == asset_1.Id); } @@ -648,10 +653,11 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture x.Id == asset_1.Id); } @@ -727,10 +733,11 @@ public class AssetTests(CreatedAppFixture fixture) : IClassFixture var contents = app.Contents(schemaName); - await contents.CreateAsync(new TestEntityData - { - Number = 1 - }); + await contents.CreateAsync( + new TestEntityData + { + Number = 1 + }); // Upload a test asset diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs index 01b60e49e..73b11ba49 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentCleanupTests.cs @@ -30,10 +30,11 @@ public class ContentCleanupTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - await contents.CreateAsync(new SimpleEntityData - { - String = "İstanbul" - }, ContentCreateOptions.AsPublish); - - await contents.CreateAsync(new SimpleEntityData - { - String = "Mersin" - }, ContentCreateOptions.AsPublish); - - await contents.CreateAsync(new SimpleEntityData - { - String = "Lüleburgaz" - }, ContentCreateOptions.AsPublish); + await contents.CreateAsync( + new SimpleEntityData + { + String = "İstanbul" + }, + ContentCreateOptions.AsPublish); + + await contents.CreateAsync( + new SimpleEntityData + { + String = "Mersin" + }, + ContentCreateOptions.AsPublish); + + await contents.CreateAsync( + new SimpleEntityData + { + String = "Lüleburgaz" + }, + ContentCreateOptions.AsPublish); // STEP 2: Get sorted contents. diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs index 207e1948c..bcf567e46 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentLanguageTests.cs @@ -21,14 +21,17 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture + var content = await _.Contents.CreateAsync( + new TestEntityData { - ["de"] = "Hallo", - ["en"] = "Hello" - } - }, ContentCreateOptions.AsPublish, QueryContext.Default.WithLanguages("de")); + Localized = new Dictionary + { + ["de"] = "Hallo", + ["en"] = "Hello" + } + }, + ContentCreateOptions.AsPublish, + QueryContext.Default.WithLanguages("de")); Assert.False(content.Data.Localized.ContainsKey("en")); Assert.Equal("Hallo", content.Data.Localized["de"]); @@ -41,15 +44,17 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture + var content = await _.Contents.CreateAsync( + new TestEntityData { - ["de"] = "Hallo", - ["en"] = "Hello", - ["custom"] = "Custom" - } - }, ContentCreateOptions.AsPublish); + Localized = new Dictionary + { + ["de"] = "Hallo", + ["en"] = "Hello", + ["custom"] = "Custom" + } + }, + ContentCreateOptions.AsPublish); // STEP 2: Get content. @@ -64,34 +69,42 @@ public class ContentLanguageTests(ContentFixture fixture) : IClassFixture + var content = await _.Contents.CreateAsync( + new TestEntityData { - ["de"] = "Hallo", - ["en"] = "Hello" - } - }, ContentCreateOptions.AsPublish); + Localized = new Dictionary + { + ["de"] = "Hallo", + ["en"] = "Hello" + } + }, + ContentCreateOptions.AsPublish); // STEP 2: Get content. var (etag1, _) = await GetEtagAsync(content.Id, []); - var (etag2, _) = await GetEtagAsync(content.Id, new Dictionary - { - ["X-Flatten"] = "1" - }); + var (etag2, _) = await GetEtagAsync( + content.Id, + new Dictionary + { + ["X-Flatten"] = "1" + }); - var (etag3, _) = await GetEtagAsync(content.Id, new Dictionary - { - ["X-Languages"] = "en" - }); + var (etag3, _) = await GetEtagAsync( + content.Id, + new Dictionary + { + ["X-Languages"] = "en" + }); - var (etag4, _) = await GetEtagAsync(content.Id, new Dictionary - { - ["X-Languages"] = "en", - ["X-Flatten"] = "1" - }); + var (etag4, _) = await GetEtagAsync( + content.Id, + new Dictionary + { + ["X-Languages"] = "en", + ["X-Flatten"] = "1" + }); static void AssertValue(string? value, string? not = null) { diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index 5d8c2dec8..5736ad33e 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -40,7 +40,35 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture x.Data.Count == 1)); + Assert.True(items_0.Items.TrueForAll(x => x.Data.Count == 1 && x.Data.ContainsKey(TestEntityData.StringField))); + } + + + [Fact] + public async Task Should_find_one() + { + var all = await _.Client.DynamicContents(_.SchemaName).GetAsync(); + + var content = await _.Client.DynamicContents(_.SchemaName).GetAsync(all.Items[0].Id); + + Assert.NotNull(content); + } + + + [Fact] + public async Task Should_find_one_with_fields() + { + var all = await _.Client.DynamicContents(_.SchemaName).GetAsync(); + + var context = + QueryContext.Default + .WithFields($"data.{TestEntityData.StringField}"); + + var content = await _.Client.DynamicContents(_.SchemaName).GetAsync(all.Items[0].Id, context); + + Assert.NotNull(content); + Assert.Single(content.Data); + Assert.Contains(TestEntityData.StringField, content.Data); } [Fact] @@ -455,13 +483,15 @@ public class ContentQueryTests(ContentQueryFixture fixture) : IClassFixture(() => { - return _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus - { - Status = "Draft", - // Ensure that the flag is true. - CheckReferrers = true - }); + return _.Contents.ChangeStatusAsync( + contentA_1.Id, + new ChangeStatus + { + Status = "Draft", + // Ensure that the flag is true. + CheckReferrers = true + }); }); // STEP 4: Delete without referrer check. - await _.Contents.ChangeStatusAsync(contentA_1.Id, new ChangeStatus - { - Status = "Draft", - // It is the default anyway, just to make it more explicit. - CheckReferrers = false - }); + await _.Contents.ChangeStatusAsync( + contentA_1.Id, + new ChangeStatus + { + Status = "Draft", + // It is the default anyway, just to make it more explicit. + CheckReferrers = false + }); } [Fact] public async Task Should_not_delete_with_bulk_when_referenced() { // STEP 1: Create a referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + var contentA_1 = await _.Contents.CreateAsync( + new TestEntityWithReferencesData + { + References = null + }, + ContentCreateOptions.AsPublish); // STEP 2: Create a content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = [contentA_1.Id] - }, ContentCreateOptions.AsPublish); + await _.Contents.CreateAsync( + new TestEntityWithReferencesData + { + References = [contentA_1.Id] + }, + ContentCreateOptions.AsPublish); // STEP 3: Try to delete with referrer check. - var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate - { - Jobs = - [ - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.Delete, - Status = "Draft" - }, - ], - CheckReferrers = true - }); + var result1 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob + { + Id = contentA_1.Id, + Type = BulkUpdateType.Delete, + Status = "Draft" + }, + ], + CheckReferrers = true + }); Assert.NotNull(result1[0].Error); // STEP 4: Delete without referrer check. - var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate - { - Jobs = - [ - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.Delete, - Status = "Draft" - }, - ], - CheckReferrers = false - }); + var result2 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob + { + Id = contentA_1.Id, + Type = BulkUpdateType.Delete, + Status = "Draft" + }, + ], + CheckReferrers = false + }); Assert.Null(result2[0].Error); } @@ -178,51 +201,57 @@ public class ContentReferencesTests(ContentReferencesFixture fixture) : IClassFi public async Task Should_not_unpublish_with_bulk_when_referenced() { // STEP 1: Create a published referenced content. - var contentA_1 = await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = null - }, ContentCreateOptions.AsPublish); + var contentA_1 = await _.Contents.CreateAsync( + new TestEntityWithReferencesData + { + References = null + }, + ContentCreateOptions.AsPublish); // STEP 2: Create a published content with a reference. - await _.Contents.CreateAsync(new TestEntityWithReferencesData - { - References = [contentA_1.Id] - }, ContentCreateOptions.AsPublish); + await _.Contents.CreateAsync( + new TestEntityWithReferencesData + { + References = [contentA_1.Id] + }, + ContentCreateOptions.AsPublish); // STEP 3: Try to delete with referrer check. - var result1 = await _.Contents.BulkUpdateAsync(new BulkUpdate - { - Jobs = - [ - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.ChangeStatus, - Status = "Draft" - }, - ], - CheckReferrers = true - }); + var result1 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob + { + Id = contentA_1.Id, + Type = BulkUpdateType.ChangeStatus, + Status = "Draft" + }, + ], + CheckReferrers = true + }); Assert.NotNull(result1[0].Error); // STEP 4: Delete without referrer check. - var result2 = await _.Contents.BulkUpdateAsync(new BulkUpdate - { - Jobs = - [ - new BulkUpdateJob - { - Id = contentA_1.Id, - Type = BulkUpdateType.ChangeStatus, - Status = "Draft" - }, - ], - CheckReferrers = false - }); + var result2 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob + { + Id = contentA_1.Id, + Type = BulkUpdateType.ChangeStatus, + Status = "Draft" + }, + ], + CheckReferrers = false + }); Assert.Null(result2[0].Error); } diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs index 87d2123dc..b15839f5c 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentScriptingTests.cs @@ -37,10 +37,11 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 13 - }); + var content = await contents.CreateAsync( + new TestEntityData + { + Number = 13 + }); Assert.Equal(26, content.Data.Number); } @@ -62,10 +63,12 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 13 - }, ContentCreateOptions.AsPublish); + var content = await contents.CreateAsync( + new TestEntityData + { + Number = 13 + }, + ContentCreateOptions.AsPublish); Assert.Equal(26, content.Data.Number); } @@ -89,10 +92,12 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - var content = await contents.CreateAsync(new TestEntityData - { - Number = 99 - }, ContentCreateOptions.AsPublish); + var content = await contents.CreateAsync( + new TestEntityData + { + Number = 99 + }, + ContentCreateOptions.AsPublish); Assert.Equal(19, content.Data.Number); } @@ -114,26 +119,27 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - var results = await contents.BulkUpdateAsync(new BulkUpdate - { - DoNotScript = false, - DoNotValidate = false, - Jobs = - [ - new BulkUpdateJob - { - Type = BulkUpdateType.Upsert, - Data = new + var results = await contents.BulkUpdateAsync( + new BulkUpdate + { + DoNotScript = false, + DoNotValidate = false, + Jobs = + [ + new BulkUpdateJob { - number = new + Type = BulkUpdateType.Upsert, + Data = new { - iv = 99 + number = new + { + iv = 99 + } } - } - }, - ], - Publish = true - }); + }, + ], + Publish = true + }); Assert.Single(results); Assert.Null(results[0].Error); @@ -162,26 +168,27 @@ public class ContentScriptingTests(CreatedAppFixture fixture) : IClassFixture(schemaName); - var results = await contents.BulkUpdateAsync(new BulkUpdate - { - DoNotScript = true, - DoNotValidate = false, - Jobs = - [ - new BulkUpdateJob - { - Type = BulkUpdateType.Upsert, - Data = new + var results = await contents.BulkUpdateAsync( + new BulkUpdate + { + DoNotScript = true, + DoNotValidate = false, + Jobs = + [ + new BulkUpdateJob { - number = new + Type = BulkUpdateType.Upsert, + Data = new { - iv = 99 + number = new + { + iv = 99 + } } - } - }, - ], - Publish = true - }); + }, + ], + Publish = true + }); Assert.Single(results); Assert.Null(results[0].Error); diff --git a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index b63a70302..c4b79cf1d 100644 --- a/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -25,17 +25,20 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture + var content = await _.Contents.CreateAsync( + new TestEntityData { - ["en"] = null - } - }, ContentCreateOptions.AsPublish); + Localized = new Dictionary + { + ["en"] = null + } + }, + ContentCreateOptions.AsPublish); // STEP 2: Get the item and ensure that the text is the same. @@ -139,13 +156,15 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture(() => { - return _.Contents.CreateAsync(new TestEntityData - { - Number = 1 - }, options); + return _.Contents.CreateAsync( + new TestEntityData + { + Number = 1 + }, + new ContentCreateOptions { Id = id, Publish = true }); }); Assert.Equal(409, ex.StatusCode); @@ -299,28 +331,35 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture + var result_0 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob { - [TestEntityData.StringField] = new - { - iv = $"{prefix}_1" - }, - [TestEntityData.NumberField] = new + Data = new Dictionary { - iv = 1 + [TestEntityData.StringField] = new + { + iv = $"{prefix}_1" + }, + [TestEntityData.NumberField] = new + { + iv = 1 + } } - } - }, - new BulkUpdateJob - { - Data = new Dictionary + }, + new BulkUpdateJob { - [TestEntityData.StringField] = new - { - iv = $"{prefix}_2" - }, - [TestEntityData.NumberField] = new + Data = new Dictionary { - iv = 2 + [TestEntityData.StringField] = new + { + iv = $"{prefix}_2" + }, + [TestEntityData.NumberField] = new + { + iv = 2 + } } - } - }, - ], - Publish = true - }); + }, + ], + Publish = true + }); result_0 = result_0.OrderBy(x => x.JobIndex).ToList(); // STEP 2: Update contents by filter. - var result_1 = await _.Contents.BulkUpdateAsync(new BulkUpdate - { - Jobs = - [ - new BulkUpdateJob - { - Query = new + var result_1 = await _.Contents.BulkUpdateAsync( + new BulkUpdate + { + Jobs = + [ + new BulkUpdateJob { - Filter = new + Query = new { - path = $"data.{TestEntityData.StringField}.iv", - op = "eq", - value = $"{prefix}_1" - } - }, - Data = new Dictionary - { - [TestEntityData.StringField] = new + Filter = new + { + path = $"data.{TestEntityData.StringField}.iv", + op = "eq", + value = $"{prefix}_1" + } + }, + Data = new Dictionary { - iv = $"{prefix}_1_x" - } + [TestEntityData.StringField] = new + { + iv = $"{prefix}_1_x" + } + }, + Type = BulkUpdateType.Patch }, - Type = BulkUpdateType.Patch - }, - new BulkUpdateJob - { - Query = new + new BulkUpdateJob { - Filter = new + Query = new { - path = $"data.{TestEntityData.StringField}.iv", - op = "eq", - value = $"{prefix}_2" - } - }, - Data = new Dictionary - { - [TestEntityData.StringField] = new + Filter = new + { + path = $"data.{TestEntityData.StringField}.iv", + op = "eq", + value = $"{prefix}_2" + } + }, + Data = new Dictionary { - iv = $"{prefix}_2_y" - } + [TestEntityData.StringField] = new + { + iv = $"{prefix}_2_y" + } + }, + Type = BulkUpdateType.Patch }, - Type = BulkUpdateType.Patch - }, - ] - }); + ] + }); result_1.OrderBy(x => x.JobIndex).Should().BeEquivalentTo( [ @@ -1221,10 +1329,11 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture x.ContentId).ToHashSet() - }); + var contents = await _.Contents.GetAsync( + new ContentQuery + { + Ids = result_0.Select(x => x.ContentId).ToHashSet() + }); var content0 = contents.Items.Find(x => x.Id == result_0[0].ContentId); var content1 = contents.Items.Find(x => x.Id == result_0[1].ContentId); @@ -1237,10 +1346,12 @@ public class ContentUpdateTests(ContentFixture fixture) : IClassFixture(schemaName); - var referencedContent = await referencedContents.CreateAsync(new TestEntityData - { - String = contentString - }); + var referencedContent = await referencedContents.CreateAsync( + new TestEntityData + { + String = contentString + }); var parentSchema = await TestEntityWithReferences.CreateSchemaAsync(app.Schemas, schemaNameRef); // Create a test content that references the other schema. var parentContents = app.Contents(schemaNameRef); - await parentContents.CreateAsync(new TestEntityWithReferencesData - { - References = - [ - referencedContent.Id - ], - }); + await parentContents.CreateAsync( + new TestEntityWithReferencesData + { + References = + [ + referencedContent.Id + ], + }); // STEP 2: Create rule. @@ -173,13 +175,15 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo var updatedString = Guid.NewGuid().ToString(); var updateEvent = "ReferenceUpdated"; - await referencedContents.UpdateAsync(referencedContent.Id, new TestEntityData - { - String = updatedString - }); + await referencedContents.UpdateAsync( + referencedContent.Id, + new TestEntityData + { + String = updatedString + }); - // Get requests. + // STEP 4: Get requests. var request = await webhookCatcher.PollAsync(sessionId, x => x.IsPost() && x.HasContent(updatedString) && x.HasContent(updateEvent)); AssertRequest(request); @@ -668,10 +672,11 @@ public class RuleRunnerTests(ClientFixture fixture, WebhookCatcherFixture webhoo // Create a test content. var contents = app.Contents(schemaName); - await contents.CreateAsync(new TestEntityData - { - String = contentString - }); + await contents.CreateAsync( + new TestEntityData + { + String = contentString + }); } private static async Task CreateAssetAsync(ISquidexClient app)