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 abb9aa2f38..7f4b90c450 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs @@ -762,6 +762,15 @@ public static string EventType { } } + /// + /// Looks up a localized string similar to The graphql request.. + /// + public static string GraphqlRequest { + get { + return ResourceManager.GetString("GraphqlRequest", resourceCulture); + } + } + /// /// Looks up a localized string similar to The current item, if the field is part of an array.. /// diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx index fb2bdafa37..ed8b9d3032 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx +++ b/backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx @@ -351,6 +351,9 @@ The type of the event. + + The graphql request. + The current item, if the field is part of an array. diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs index bae1f0394d..8330ca44ff 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentHeaders.cs @@ -23,6 +23,7 @@ public static class ContentHeaders public const string KeyNoResolveLanguages = "X-NoResolveLanguages"; public const string KeyResolveFlow = "X-ResolveFlow"; public const string KeyResolveUrls = "X-ResolveUrls"; + public const string KeyResolveSchemaNames = "X-ResolveSchemaName"; public const string KeyUnpublished = "X-Unpublished"; public static void AddCacheHeaders(this Context context, IRequestCache cache) @@ -98,6 +99,16 @@ public static ICloneBuilder WithResolveFlow(this ICloneBuilder builder, bool val return builder.WithBoolean(KeyResolveFlow, value); } + public static bool ResolveSchemaNames(this Context context) + { + return context.AsBoolean(KeyResolveSchemaNames); + } + + public static ICloneBuilder WithResolveSchemaNames(this ICloneBuilder builder, bool value = true) + { + return builder.WithBoolean(KeyResolveSchemaNames, value); + } + public static bool NoResolveLanguages(this Context context) { return context.AsBoolean(KeyNoResolveLanguages); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index faf3609b24..3edc45f98d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -41,6 +41,7 @@ public GraphQLExecutionContext( this.dataLoaders = dataLoaders; Context = context.Clone(b => b + .WithResolveSchemaNames() .WithNoCleanup() .WithNoEnrichment() .WithNoAssetEnrichment()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs index 190cb3dacb..200b1a6c2c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ApplicationQueries.cs @@ -6,7 +6,9 @@ // ========================================================================== using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; +using static Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents.ContentActions; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; @@ -28,34 +30,62 @@ public ApplicationQueries(Builder builder, IEnumerable schemaInfos) continue; } - AddContentFind(schemaInfo, contentType); - AddContentQueries(builder, schemaInfo, contentType); + if (schemaInfo.Schema.SchemaDef.Type == SchemaType.Singleton) + { + // Mark the normal queries as deprecated to motivate using the new endpoint. + var deprecation = $"Use 'find{schemaInfo.TypeName}Singleton' instead."; + + AddContentFind(schemaInfo, contentType, deprecation); + AddContentFindSingleton(schemaInfo, contentType); + AddContentQueries(builder, schemaInfo, contentType, deprecation); + } + else + { + AddContentFind(schemaInfo, contentType, null); + AddContentQueries(builder, schemaInfo, contentType, null); + } } Description = "The app queries."; } - private void AddContentFind(SchemaInfo schemaInfo, IGraphType contentType) + private void AddContentFind(SchemaInfo schemaInfo, IGraphType contentType, string? deprecatedReason) { AddField(new FieldTypeWithSchemaId { Name = $"find{schemaInfo.TypeName}Content", - Arguments = ContentActions.Find.Arguments, + Arguments = Find.Arguments, ResolvedType = contentType, - Resolver = ContentActions.Find.Resolver, + Resolver = Find.Resolver, + DeprecationReason = deprecatedReason, Description = $"Find an {schemaInfo.DisplayName} content by id.", SchemaId = schemaInfo.Schema.Id }); } - private void AddContentQueries(Builder builder, SchemaInfo schemaInfo, IGraphType contentType) + private void AddContentFindSingleton(SchemaInfo schemaInfo, IGraphType contentType) + { + AddField(new FieldTypeWithSchemaId + { + Name = $"find{schemaInfo.TypeName}Singleton", + Arguments = FindSingleton.Arguments, + ResolvedType = contentType, + Resolver = FindSingleton.Resolver, + DeprecationReason = null, + Description = $"Find an {schemaInfo.DisplayName} singleton.", + SchemaId = schemaInfo.Schema.Id + }); + } + + private void AddContentQueries(Builder builder, SchemaInfo schemaInfo, IGraphType contentType, string? deprecatedReason) { AddField(new FieldTypeWithSchemaId { Name = $"query{schemaInfo.TypeName}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments, + Arguments = QueryOrReferencing.Arguments, ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ContentActions.QueryOrReferencing.Query, + Resolver = QueryOrReferencing.Query, + DeprecationReason = deprecatedReason, Description = $"Query {schemaInfo.DisplayName} content items.", SchemaId = schemaInfo.Schema.Id }); @@ -70,9 +100,10 @@ private void AddContentQueries(Builder builder, SchemaInfo schemaInfo, IGraphTyp AddField(new FieldTypeWithSchemaId { Name = $"query{schemaInfo.TypeName}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments, + Arguments = QueryOrReferencing.Arguments, ResolvedType = contentResultTyp, - Resolver = ContentActions.QueryOrReferencing.QueryWithTotal, + Resolver = QueryOrReferencing.QueryWithTotal, + DeprecationReason = deprecatedReason, Description = $"Query {schemaInfo.DisplayName} content items with total count.", SchemaId = schemaInfo.Schema.Id }); @@ -90,9 +121,9 @@ private void AddContentQuery(Builder builder) AddField(new FieldType { Name = "queryContentsByIds", - Arguments = ContentActions.QueryByIds.Arguments, + Arguments = QueryByIds.Arguments, ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(unionType))), - Resolver = ContentActions.QueryByIds.Resolver, + Resolver = QueryByIds.Resolver, Description = "Query content items by IDs across schemeas." }); } 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 bda4e47f03..949aacf554 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 @@ -96,6 +96,36 @@ public static class Find }); } + public static class FindSingleton + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(Scalars.Int) + { + Name = "version", + Description = FieldDescriptions.QueryVersion, + DefaultValue = null + } + }; + + public static readonly IFieldResolver Resolver = Resolvers.Sync((_, fieldContext, context) => + { + var contentSchemaId = fieldContext.FieldDefinition.SchemaId(); + var contentVersion = fieldContext.GetArgument("version"); + + if (contentVersion >= 0) + { + return context.GetContent(contentSchemaId, contentSchemaId, contentVersion.Value); + } + else + { + return context.GetContent(contentSchemaId, contentSchemaId, + fieldContext.FieldNames(), + fieldContext.CacheDuration()); + } + }); + } + public static class QueryByIds { public static readonly QueryArguments Arguments = new QueryArguments 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 7327fcafe8..82a6685a8a 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 @@ -149,7 +149,10 @@ private ContentConverter GenerateConverter(Context context, ResolvedComponents c { converter.Add(new ResolveAssetUrls(context.App.NamedId(), urlGenerator, assetUrls)); } + } + if (!context.IsFrontendClient || context.ResolveSchemaNames()) + { converter.Add(new AddSchemaNames(components)); } diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptAnyBodyAttribute.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptAnyBodyAttribute.cs new file mode 100644 index 0000000000..0fb3164922 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptAnyBodyAttribute.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; +using NSwag; +using NSwag.Annotations; +using NSwag.Generation.Processors; +using NSwag.Generation.Processors.Contexts; +using Squidex.Domain.Apps.Core; + +namespace Squidex.Areas.Api.Config.OpenApi; + +public sealed class AcceptAnyBodyAttribute : OpenApiOperationProcessorAttribute +{ + public AcceptAnyBodyAttribute() + : base(typeof(Processor)) + { + } + + public sealed class Processor : IOperationProcessor + { + public bool Process(OperationProcessorContext context) + { + context.OperationDescription.Operation.Parameters.Add( + new OpenApiParameter + { + Name = "request", + Kind = OpenApiParameterKind.Body, + Schema = new JsonSchema + { + }, + Description = FieldDescriptions.GraphqlRequest + }); + + return true; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptQueryAttribute.cs b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptQueryAttribute.cs index 8fc6300004..adb8b66bca 100644 --- a/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptQueryAttribute.cs +++ b/backend/src/Squidex/Areas/Api/Config/OpenApi/AcceptQueryAttribute.cs @@ -18,18 +18,13 @@ public AcceptQueryAttribute(bool supportsSearch) { } - public sealed class Processor : IOperationProcessor +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + public sealed record Processor(bool SupportsSearch) : IOperationProcessor +#pragma warning restore SA1313 // Parameter names should begin with lower-case letter { - private readonly bool supportsSearch; - - public Processor(bool supportsSearch) - { - this.supportsSearch = supportsSearch; - } - public bool Process(OperationProcessorContext context) { - context.OperationDescription.Operation.AddQuery(supportsSearch); + context.OperationDescription.Operation.AddQuery(SupportsSearch); return true; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs index 93a2c54cc2..3fd0950636 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsSharedController.cs @@ -21,6 +21,7 @@ namespace Squidex.Areas.Api.Controllers.Contents; [SchemaMustBePublished] +[ApiExplorerSettings(GroupName = nameof(Contents))] public sealed class ContentsSharedController : ApiController { private readonly IContentQueryService contentQuery; @@ -39,18 +40,97 @@ public ContentsSharedController(ICommandBus commandBus, /// GraphQL endpoint. /// /// The name of the app. + /// The request parameters. /// Contents returned or mutated.. /// App not found.. /// /// You can read the generated documentation for your app at /api/content/{appName}/docs. /// [Route("content/{app}/graphql/")] - [Route("content/{app}/graphql/batch")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous] [ApiCosts(2)] [AcceptHeader.Unpublished] [IgnoreCacheFilter] - public IActionResult GetGraphQL(string app) + public IActionResult GetGraphQL(string app, GraphQLQueryDto request) + { + var options = new GraphQLHttpMiddlewareOptions + { + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; + + return new GraphQLExecutionActionResult(options); + } + + /// + /// GraphQL endpoint. + /// + /// The name of the app. + /// Contents returned or mutated.. + /// App not found.. + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPost("content/{app}/graphql/")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(2)] + [AcceptAnyBody] + [AcceptHeader.Unpublished] + [IgnoreCacheFilter] + public IActionResult PostGraphQL(string app) + { + var options = new GraphQLHttpMiddlewareOptions + { + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; + + return new GraphQLExecutionActionResult(options); + } + + /// + /// GraphQL batch endpoint. + /// + /// The name of the app. + /// The request object. + /// Contents returned or mutated.. + /// App not found.. + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpGet("content/{app}/graphql/batch")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(2)] + [AcceptHeader.Unpublished] + [IgnoreCacheFilter] + public IActionResult GetGraphQLBatch(string app, GraphQLQueryDto request) + { + var options = new GraphQLHttpMiddlewareOptions + { + DefaultResponseContentType = new MediaTypeHeaderValue("application/json") + }; + + return new GraphQLExecutionActionResult(options); + } + + /// + /// GraphQL batch endpoint. + /// + /// The name of the app. + /// Contents returned or mutated.. + /// App not found.. + /// + /// You can read the generated documentation for your app at /api/content/{appName}/docs. + /// + [HttpPost("content/{app}/graphql/batch")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ApiPermissionOrAnonymous] + [ApiCosts(2)] + [AcceptAnyBody] + [AcceptHeader.Unpublished] + [IgnoreCacheFilter] + public IActionResult PostGraphQLBatch(string app) { var options = new GraphQLHttpMiddlewareOptions { @@ -143,7 +223,7 @@ public async Task GetAllContentsPost(string app, [FromBody] AllCo [ProducesResponseType(typeof(BulkResultDto[]), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(PermissionIds.AppContentsReadOwn)] [ApiCosts(5)] - public async Task BulkUpdateContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) + public async Task BulkUpdateAllContents(string app, string schema, [FromBody] BulkUpdateContentsDto request) { var command = request.ToCommand(true); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLQueryDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLQueryDto.cs new file mode 100644 index 0000000000..8e11f52f69 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLQueryDto.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Mvc; + +namespace Squidex.Areas.Api.Controllers.Contents.Models; + +public sealed class GraphQLQueryDto +{ + /// + /// The optional version of the asset. + /// + [FromQuery(Name = "The query string")] + public string Query { get; set; } + + /// + /// The optional operation variables. + /// + [FromQuery(Name = "variables")] + public string? Variables { get; set; } + + /// + /// The optional operation name. + /// + [FromQuery(Name = "operationName")] + public string? OperationName { get; set; } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 79666087b5..db79711dc5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -53,7 +53,7 @@ public async Task Should_query_contents_with_full_text() var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.Matches(x => x.QueryAsOdata == "?$skip=0&$search=\"Hello\"" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, content)); @@ -345,7 +345,7 @@ public async Task Should_return_multiple_flat_contents_if_querying_contents() var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, content)); @@ -384,7 +384,7 @@ public async Task Should_return_multiple_contents_if_querying_contents() var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && x.NoTotal), A._)) .Returns(ResultList.CreateFrom(0, content)); @@ -423,7 +423,7 @@ public async Task Should_return_multiple_contents_with_total_if_querying_content var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), A.That.Matches(x => x.QueryAsOdata == "?$top=30&$skip=5" && !x.NoTotal), A._)) .Returns(ResultList.CreateFrom(10, content)); @@ -503,7 +503,7 @@ public async Task Should_return_null_if_single_content_not_found() public async Task Should_return_null_if_single_content_from_another_schema() { var contentId = DomainId.NewGuid(); - var content = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentId, "reference1-field", "reference1"); + var content = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentId, "reference1-field", "reference1"); A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A.That.HasIdsWithoutTotal(contentId), @@ -537,7 +537,7 @@ public async Task Should_return_null_if_single_content_from_another_schema() } [Fact] - public async Task Should_return_single_content_if_finding_content() + public async Task Should_find_single_content() { var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); @@ -574,12 +574,12 @@ public async Task Should_return_single_content_if_finding_content() } [Fact] - public async Task Should_return_single_content_if_finding_content_with_version() + public async Task Should_find_single_content_with_version() { var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId); - A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), TestSchemas.Default.Id.ToString(), contentId, 3, + A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), contentId, 3, A._)) .Returns(content); @@ -609,11 +609,102 @@ public async Task Should_return_single_content_if_finding_content_with_version() AssertResult(expected, actual); } + [Fact] + public async Task Should_find_singleton_content() + { + var contentId = TestSchemas.Singleton.Id; + var content = TestContent.CreateSimple(TestSchemas.Singleton.NamedId(), contentId, "singleton-field", "Hello"); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), + A.That.HasIdsWithoutTotal(contentId), + A._)) + .Returns(ResultList.CreateFrom(1, content)); + + var actual = await ExecuteAsync(new TestQuery + { + Query = @" + query { + findMySingletonSingleton { + id, + flatData { + singletonField + } + } + }", + Args = new + { + contentId + } + }); + + var expected = new + { + data = new + { + findMySingletonSingleton = new + { + id = contentId, + flatData = new + { + singletonField = "Hello" + } + } + } + }; + + AssertResult(expected, actual); + } + + [Fact] + public async Task Should_find_singleton_content_with_version() + { + var contentId = TestSchemas.Singleton.Id; + var content = TestContent.CreateSimple(TestSchemas.Singleton.NamedId(), contentId, "singleton-field", "Hello"); + + A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), content.SchemaId.Id.ToString(), contentId, 3, + A._)) + .Returns(content); + + var actual = await ExecuteAsync(new TestQuery + { + Query = @" + query { + findMySingletonSingleton(version: 3) { + id, + flatData { + singletonField + } + } + }", + Args = new + { + contentId + } + }); + + var expected = new + { + data = new + { + findMySingletonSingleton = new + { + id = contentId, + flatData = new + { + singletonField = "Hello" + } + } + } + }; + + AssertResult(expected, actual); + } + [Fact] public async Task Should_also_fetch_embedded_contents_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -703,7 +794,7 @@ ... on MyReference1 { public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -782,7 +873,7 @@ public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_ public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -844,7 +935,7 @@ public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_ public async Task Should_cache_referenced_contents_from_flat_data_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -915,7 +1006,7 @@ myReferences @cache(duration: 1000) { public async Task Should_also_fetch_referencing_contents_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -984,7 +1075,7 @@ public async Task Should_also_fetch_referencing_contents_if_field_is_included_in public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -1060,7 +1151,7 @@ public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_ public async Task Should_also_fetch_references_contents_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -1117,7 +1208,7 @@ public async Task Should_also_fetch_references_contents_if_field_is_included_in_ public async Task Should_also_fetch_references_contents_with_total_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); @@ -1181,7 +1272,7 @@ public async Task Should_also_fetch_references_contents_with_total_if_field_is_i public async Task Should_also_fetch_union_contents_if_field_is_included_in_query() { var contentRefId = DomainId.NewGuid(); - var contentRef = TestContent.CreateRef(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); + var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1"); var contentId = DomainId.NewGuid(); var content = TestContent.Create(contentId, contentRefId); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 50be7d694a..508bd5553b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -76,7 +76,12 @@ protected void AssertResult(object expected, ExecutionResult actual) protected Task ExecuteAsync(TestQuery query) { // Use a shared instance to test caching. - sut ??= CreateSut(TestSchemas.Default, TestSchemas.Reference1, TestSchemas.Reference2, TestSchemas.Component); + sut ??= CreateSut( + TestSchemas.Default, + TestSchemas.Reference1, + TestSchemas.Reference2, + TestSchemas.Singleton, + TestSchemas.Component); var options = query.ToOptions(sut.Services); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs index a593e3993e..c359234902 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs @@ -346,7 +346,7 @@ public static IEnrichedContentEntity Create(DomainId id, DomainId refId = defaul return content; } - public static IEnrichedContentEntity CreateRef(NamedId schemaId, DomainId id, string field, string value) + public static IEnrichedContentEntity CreateSimple(NamedId schemaId, DomainId id, string field, string value) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs index 0946c4d6ca..cb6501228e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs @@ -19,6 +19,7 @@ public static class TestSchemas public static readonly ISchemaEntity Default; public static readonly ISchemaEntity Reference1; public static readonly ISchemaEntity Reference2; + public static readonly ISchemaEntity Singleton; public static readonly ISchemaEntity Component; static TestSchemas() @@ -57,6 +58,11 @@ type JsonNested { .Publish() .AddString(1, "reference2-field", Partitioning.Invariant)); + Singleton = Mocks.Schema(TestApp.DefaultId, DomainId.NewGuid(), + new Schema("my-singleton", type: SchemaType.Singleton) + .Publish() + .AddString(1, "singleton-field", Partitioning.Invariant)); + Default = Mocks.Schema(TestApp.DefaultId, DomainId.NewGuid(), new Schema("my-schema") .Publish()