From 5c81242e1a0e93db9f778971312b812f07a80209 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 Oct 2019 17:30:34 +0200 Subject: [PATCH 01/14] Multiple references in core model. --- .../Schemas/ReferencesFieldProperties.cs | 19 ++++++- .../Schemas/SchemaExtensions.cs | 6 ++- .../ExtractReferenceIds/ReferencesCleaner.cs | 2 +- .../ReferencesExtractor.cs | 7 ++- .../FieldValueValidatorsFactory.cs | 5 +- .../ValidateContent/ValidationContext.cs | 44 +++++++++++++--- .../Validators/ReferencesValidator.cs | 22 ++++---- .../Validators/UniqueValidator.cs | 2 +- .../UserManagerExtensions.cs | 1 + .../ValidateContent/NumberFieldTests.cs | 2 +- .../ValidateContent/ReferencesFieldTests.cs | 51 +++++++++++++++---- .../ValidateContent/StringFieldTests.cs | 2 +- .../ValidateContent/UIFieldTests.cs | 1 + .../ValidationTestExtensions.cs | 15 +++--- .../Validators/UniqueValidatorTests.cs | 6 ++- .../Squidex.Domain.Apps.Core.Tests.csproj | 1 - .../TestUtils.cs | 11 +++- 17 files changed, 148 insertions(+), 49 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs index 3f6013f9e..bca51a0cc 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; namespace Squidex.Domain.Apps.Core.Schemas { @@ -21,7 +23,22 @@ namespace Squidex.Domain.Apps.Core.Schemas public ReferencesFieldEditor Editor { get; set; } - public Guid SchemaId { get; set; } + public ReadOnlyCollection SchemaIds { get; set; } + + public Guid SchemaId + { + set + { + if (value != default) + { + SchemaIds = new ReadOnlyCollection(new List { value }); + } + else + { + SchemaIds = null; + } + } + } public override T Accept(IFieldPropertiesVisitor visitor) { diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs index 0e2274691..333ad6d05 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs @@ -54,11 +54,15 @@ namespace Squidex.Domain.Apps.Core.Schemas return schema.Properties.Label.WithFallback(schema.TypeName()); } + public static Guid SingleId(this ReferencesFieldProperties properties) + { + return properties.SchemaIds?.Count == 1 ? properties.SchemaIds[0] : Guid.Empty; + } + public static IEnumerable> ResolvingReferences(this Schema schema) { return schema.Fields.OfType>() .Where(x => - x.Properties.SchemaId != Guid.Empty && x.Properties.ResolveReference && x.Properties.MaxItems == 1 && (x.Properties.IsListField || schema.Fields.Count == 1)); diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index b8c0e33e9..240dffb99 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds public IJsonValue Visit(IField field) { - if (oldReferences.Contains(field.Properties.SchemaId)) + if (oldReferences.Contains(field.Properties.SingleId())) { return JsonValue.Array(); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index df024acc0..90fdb52a6 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -62,9 +62,12 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds { var ids = value.ToGuidSet(); - if (strategy == Ids.All && field.Properties.SchemaId != Guid.Empty) + if (strategy == Ids.All && field.Properties.SchemaIds != null) { - ids.Add(field.Properties.SchemaId); + foreach (var schemaId in field.Properties.SchemaIds) + { + ids.Add(schemaId); + } } return ids; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs index a73183637..95e179558 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/FieldValueValidatorsFactory.cs @@ -134,10 +134,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new UniqueValuesValidator(); } - if (field.Properties.SchemaId != Guid.Empty) - { - yield return new ReferencesValidator(field.Properties.SchemaId); - } + yield return new ReferencesValidator(field.Properties.SchemaIds); } public IEnumerable Visit(IField field) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs index bf52c5824..ec4740c39 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidationContext.cs @@ -14,7 +14,9 @@ using Squidex.Infrastructure.Queries; namespace Squidex.Domain.Apps.Core.ValidateContent { - public delegate Task> CheckContents(Guid schemaId, FilterNode filter); + public delegate Task> CheckContents(Guid schemaId, FilterNode filter); + + public delegate Task> CheckContentsByIds(HashSet ids); public delegate Task> CheckAssets(IEnumerable ids); @@ -23,6 +25,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private readonly Guid contentId; private readonly Guid schemaId; private readonly CheckContents checkContent; + private readonly CheckContentsByIds checkContentByIds; private readonly CheckAssets checkAsset; private readonly ImmutableQueue propertyPath; @@ -47,8 +50,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent Guid contentId, Guid schemaId, CheckContents checkContent, + CheckContentsByIds checkContentsByIds, CheckAssets checkAsset) - : this(contentId, schemaId, checkContent, checkAsset, ImmutableQueue.Empty, false) + : this(contentId, schemaId, checkContent, checkContentsByIds, checkAsset, ImmutableQueue.Empty, false) { } @@ -56,16 +60,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent Guid contentId, Guid schemaId, CheckContents checkContent, + CheckContentsByIds checkContentByIds, CheckAssets checkAsset, ImmutableQueue propertyPath, bool isOptional) { Guard.NotNull(checkAsset, nameof(checkAsset)); - Guard.NotNull(checkContent, nameof(checkAsset)); + Guard.NotNull(checkContent, nameof(checkContent)); + Guard.NotNull(checkContentByIds, nameof(checkContentByIds)); this.propertyPath = propertyPath; this.checkContent = checkContent; + this.checkContentByIds = checkContentByIds; this.checkAsset = checkAsset; this.contentId = contentId; @@ -76,17 +83,40 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public ValidationContext Optional(bool isOptional) { - return isOptional == IsOptional ? this : new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath, isOptional); + return isOptional == IsOptional ? this : OptionalCore(isOptional); + } + + private ValidationContext OptionalCore(bool isOptional) + { + return new ValidationContext( + contentId, + schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath, + isOptional); } public ValidationContext Nested(string property) { - return new ValidationContext(contentId, schemaId, checkContent, checkAsset, propertyPath.Enqueue(property), IsOptional); + return new ValidationContext( + contentId, schemaId, + checkContent, + checkContentByIds, + checkAsset, + propertyPath.Enqueue(property), + IsOptional); + } + + public Task> GetContentIdsAsync(HashSet ids) + { + return checkContentByIds(ids); } - public Task> GetContentIdsAsync(Guid validatedSchemaId, FilterNode filter) + public Task> GetContentIdsAsync(Guid schemaId, FilterNode filter) { - return checkContent(validatedSchemaId, filter); + return checkContent(schemaId, filter); } public Task> GetAssetInfosAsync(IEnumerable assetId) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs index 91066eab2..62ad9a34c 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/ReferencesValidator.cs @@ -9,35 +9,37 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Squidex.Infrastructure.Queries; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { public sealed class ReferencesValidator : IValidator { - private static readonly PropertyPath Path = "Id"; + private readonly IEnumerable schemaIds; - private readonly Guid schemaId; - - public ReferencesValidator(Guid schemaId) + public ReferencesValidator(IEnumerable schemaIds) { - this.schemaId = schemaId; + this.schemaIds = schemaIds; } public async Task ValidateAsync(object value, ValidationContext context, AddError addError) { if (value is ICollection contentIds) { - var filter = ClrFilter.In(Path, contentIds.ToList()); - - var foundIds = await context.GetContentIdsAsync(schemaId, filter); + var foundIds = await context.GetContentIdsAsync(contentIds.ToHashSet()); foreach (var id in contentIds) { - if (!foundIds.Contains(id)) + var (schemaId, _) = foundIds.FirstOrDefault(x => x.Id == id); + + if (schemaId == Guid.Empty) { addError(context.Path, $"Contains invalid reference '{id}'."); } + else if (schemaIds?.Any() == true && !schemaIds.Contains(schemaId)) + { + addError(context.Path, $"Contains reference '{id}' to invalid schema."); + } } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs index 6717f242b..6fad491b9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueValidator.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators { var found = await context.GetContentIdsAsync(context.SchemaId, filter); - if (found.Any(x => x != context.ContentId)) + if (found.Any(x => x.Id != context.ContentId)) { addError(context.Path, "Another content with the same value exists."); } diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index 880663a92..e89160674 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Security.Claims; diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs index 1be28f5b9..9e42b5821 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new NumberFieldProperties { IsUnique = true }); - await sut.ValidateAsync(CreateValue(12.5), errors, ValidationTestExtensions.References(Guid.NewGuid())); + await sut.ValidateAsync(CreateValue(12.5), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); errors.Should().BeEquivalentTo( new[] { "Another content with the same value exists." }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs index 1e6c5ce66..e3d21769d 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -36,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References(ref1)); + await sut.ValidateAsync(CreateValue(ref1), errors, Context()); Assert.Empty(errors); } @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors, Context()); Assert.Empty(errors); } @@ -56,7 +57,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); Assert.Empty(errors); } @@ -66,7 +67,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { MinItems = 2, MaxItems = 2, AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, Context()); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_schemas_not_defined() + { + var sut = Field(new ReferencesFieldProperties()); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); Assert.Empty(errors); } @@ -76,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(null), errors, Context()); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -87,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, IsRequired = true }); - await sut.ValidateAsync(CreateValue(), errors); + await sut.ValidateAsync(CreateValue(), errors, Context()); errors.Should().BeEquivalentTo( new[] { "Field is required." }); @@ -98,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties()); - await sut.ValidateAsync(JsonValue.Create("invalid"), errors); + await sut.ValidateAsync(JsonValue.Create("invalid"), errors, Context()); errors.Should().BeEquivalentTo( new[] { "Not a valid value." }); @@ -109,7 +120,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MinItems = 3 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, ValidationTestExtensions.References(ref1, ref2)); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); errors.Should().BeEquivalentTo( new[] { "Must have at least 3 item(s)." }); @@ -120,7 +131,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId, MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(ref1, ref2), errors, ValidationTestExtensions.References(ref1, ref2)); + await sut.ValidateAsync(CreateValue(ref1, ref2), errors, Context()); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); @@ -137,12 +148,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { $"Contains invalid reference '{ref1}'." }); } + [Fact] + public async Task Should_add_error_if_reference_schema_is_not_valid() + { + var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); + + await sut.ValidateAsync(CreateValue(ref1), errors, ValidationTestExtensions.References((Guid.NewGuid(), ref1))); + + errors.Should().BeEquivalentTo( + new[] { $"Contains reference '{ref1}' to invalid schema." }); + } + [Fact] public async Task Should_add_error_if_reference_contains_duplicate_values() { var sut = Field(new ReferencesFieldProperties { SchemaId = schemaId }); - await sut.ValidateAsync(CreateValue(ref1, ref1), errors, ValidationTestExtensions.References(ref1)); + await sut.ValidateAsync(CreateValue(ref1, ref1), errors, + ValidationTestExtensions.References( + (schemaId, ref1))); errors.Should().BeEquivalentTo( new[] { "Must not contain duplicate values." }); @@ -153,6 +177,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent return ids == null ? JsonValue.Null : JsonValue.Array(ids.Select(x => (object)x.ToString()).ToArray()); } + private ValidationContext Context() + { + return ValidationTestExtensions.References( + (schemaId, ref1), + (schemaId, ref2)); + } + private static RootField Field(ReferencesFieldProperties properties) { return Fields.References(1, "my-refs", Partitioning.Invariant, properties); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index 69d9402e4..db99fcede 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -120,7 +120,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var sut = Field(new StringFieldProperties { IsUnique = true }); - await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References(Guid.NewGuid())); + await sut.ValidateAsync(CreateValue("abc"), errors, ValidationTestExtensions.References((Guid.NewGuid(), Guid.NewGuid()))); errors.Should().BeEquivalentTo( new[] { "Another content with the same value exists." }); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index 8f7324897..bb22186fe 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -110,6 +110,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent Guid.NewGuid(), Guid.NewGuid(), (c, s) => null, + (s) => null, (c) => null); var validator = new ContentValidator(schema, x => InvariantPartitioning.Instance, validationContext); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index 43c17e712..66885c301 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -17,10 +17,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { public static class ValidationTestExtensions { - private static readonly Task> EmptyReferences = Task.FromResult>(new List()); + private static readonly Task> EmptyReferences = Task.FromResult>(new List<(Guid SchemaId, Guid Id)>()); private static readonly Task> EmptyAssets = Task.FromResult>(new List()); - public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyAssets); + public static readonly ValidationContext ValidContext = new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), + (x, y) => EmptyReferences, + (x) => EmptyReferences, + (x) => EmptyAssets); public static Task ValidateAsync(this IValidator validator, object value, IList errors, ValidationContext context = null) { @@ -70,14 +73,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { var actual = Task.FromResult>(assets.ToList()); - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => actual); + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => EmptyReferences, x => EmptyReferences, x => actual); } - public static ValidationContext References(params Guid[] referencesIds) + public static ValidationContext References(params (Guid Id, Guid SchemaId)[] referencesIds) { - var actual = Task.FromResult>(referencesIds.ToList()); + var actual = Task.FromResult>(referencesIds.ToList()); - return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => EmptyAssets); + return new ValidationContext(Guid.NewGuid(), Guid.NewGuid(), (x, y) => actual, x => actual, x => EmptyAssets); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs index 3c8e72881..7beb00d3a 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs @@ -82,7 +82,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { filter(filterNode.ToString()); - return Task.FromResult>(new List { id }); + return Task.FromResult>(new List<(Guid, Guid)> { (schemaId, id) }); + }, + (ids) => + { + return Task.FromResult>(new List<(Guid, Guid)> { (schemaId, id) }); }, ids => { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index e6346285c..ce93b49d2 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -9,7 +9,6 @@ - diff --git a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs index 7b50b28ff..2a3cb8c31 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs @@ -129,7 +129,14 @@ namespace Squidex.Domain.Apps.Core public static void TestFreeze(IFreezable sut) { - foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + var properties = + sut.GetType().GetRuntimeProperties() + .Where(x => + x.CanWrite && + x.CanRead && + x.Name != "IsFrozen"); + + foreach (var property in properties) { var value = property.PropertyType.IsValueType ? @@ -145,7 +152,7 @@ namespace Squidex.Domain.Apps.Core sut.Freeze(); - foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + foreach (var property in properties) { var value = property.PropertyType.IsValueType ? From 6035db8fc031f7ce8a06db0a325ff62afa95a3b4 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 Oct 2019 19:10:28 +0200 Subject: [PATCH 02/14] Started with GraphQL. --- .../Contents/MongoContentCollection.cs | 15 +- .../Contents/MongoContentRepository.cs | 10 +- .../Contents/ContentEntity.cs | 2 +- .../Contents/ContentOperationContext.cs | 12 +- .../GraphQL/GraphQLExecutionContext.cs | 14 +- .../Contents/GraphQL/GraphQLModel.cs | 8 +- .../Contents/GraphQL/IGraphModel.cs | 4 +- .../GraphQL/Types/AppQueriesGraphType.cs | 6 +- .../GraphQL/Types/ContentDataGraphType.cs | 12 +- .../GraphQL/Types/ContentGraphType.cs | 86 +++++++++ .../Types/ContentInterfaceGraphType.cs | 84 +++++++++ .../GraphQL/Types/QueryGraphTypeVisitor.cs | 30 ++- .../GraphQL/Types/ReferenceGraphType.cs | 61 ++++++ .../Contents/Queries/ContentEnricher.cs | 26 ++- .../Contents/Queries/QueryExecutionContext.cs | 4 +- .../Repositories/IContentRepository.cs | 4 +- .../IEntityWithCacheDependencies.cs | 2 +- .../Contents/GraphQL/GraphQLQueriesTests.cs | 177 ++++++++++++++++-- .../Contents/GraphQL/GraphQLTestBase.cs | 60 +++++- .../Queries/ContentEnricherReferencesTests.cs | 37 ++-- .../Contents/Queries/ContentEnricherTests.cs | 6 +- 21 files changed, 568 insertions(+), 92 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs create mode 100644 src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs index 47638300c..7988b72a7 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs @@ -169,15 +169,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents }); } - public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) + public async Task> QueryIdsAsync(ISchemaEntity schema, FilterNode filterNode) { var filter = filterNode.AdjustToModel(schema.SchemaDef, true).ToFilter(schema.Id); var contentEntities = - await Collection.Find(filter).Only(x => x.Id) + await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) .ToListAsync(); - return contentEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); + } + + public async Task> QueryIdsAsync(HashSet ids) + { + var contentEntities = + await Collection.Find(Filter.In(x => x.Id, ids)).Only(x => x.Id, x => x.IndexedSchemaId) + .ToListAsync(); + + return contentEntities.Select(x => (Guid.Parse(x["_si"].AsString), Guid.Parse(x["_id"].AsString))).ToList(); } public async Task> QueryIdsAsync(Guid appId) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 2e78ae822..c7d13da72 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } - public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) + public async Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode) { using (Profiler.TraceMethod()) { @@ -124,6 +124,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents } } + public async Task> QueryIdsAsync(Guid appId, HashSet ids) + { + using (Profiler.TraceMethod()) + { + return await contents.QueryIdsAsync(ids); + } + } + public async Task QueryScheduledWithoutDataAsync(Instant now, Func callback) { using (Profiler.TraceMethod()) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 5f91f13ea..e5f9c4647 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -49,6 +49,6 @@ namespace Squidex.Domain.Apps.Entities.Contents public bool IsPending { get; set; } - public HashSet CacheDependencies { get; set; } + public HashSet CacheDependencies { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index 28d9b75e9..d5752543a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs @@ -114,7 +114,10 @@ namespace Squidex.Domain.Apps.Entities.Contents private ValidationContext CreateValidationContext() { - return new ValidationContext(command.ContentId, schemaId, QueryContentsAsync, QueryAssetsAsync); + return new ValidationContext(command.ContentId, schemaId, + QueryContentsAsync, + QueryContentsAsync, + QueryAssetsAsync); } private async Task> QueryAssetsAsync(IEnumerable assetIds) @@ -122,11 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Contents return await assetRepository.QueryAsync(appEntity.Id, new HashSet(assetIds)); } - private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) + private async Task> QueryContentsAsync(Guid filterSchemaId, FilterNode filterNode) { return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode); } + private async Task> QueryContentsAsync(HashSet ids) + { + return await contentRepository.QueryIdsAsync(appEntity.Id, ids); + } + private string GetScript(Func script) { return script(schemaEntity.SchemaDef.Scripts); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index b128aabae..f5a27ffd9 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -60,9 +60,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return dataLoader.LoadAsync(id); } - public override Task FindContentAsync(Guid schemaId, Guid id) + public Task FindContentAsync(Guid id) { - var dataLoader = GetContentsLoader(schemaId); + var dataLoader = GetContentsLoader(); return dataLoader.LoadAsync(id); } @@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return await dataLoader.LoadManyAsync(ids); } - public async Task> GetReferencedContentsAsync(Guid schemaId, IJsonValue value) + public async Task> GetReferencedContentsAsync(IJsonValue value) { var ids = ParseIds(value); @@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return EmptyContents; } - var dataLoader = GetContentsLoader(schemaId); + var dataLoader = GetContentsLoader(); return await dataLoader.LoadManyAsync(ids); } @@ -106,12 +106,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }); } - private IDataLoader GetContentsLoader(Guid schemaId) + private IDataLoader GetContentsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"Schema_{schemaId}", + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader($"References", async batch => { - var result = await GetReferencedContentsAsync(schemaId, new List(batch)); + var result = await GetReferencedContentsAsync(new List(batch)); return result.ToDictionary(x => x.Id); }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index cf4adb1be..ddb6c9fd8 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -137,15 +137,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) { - return field.Accept(new QueryGraphTypeVisitor(schema, GetContentType, this, assetListType, fieldName)); + return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, GetContentType, this, assetListType, fieldName)); } - public IGraphType GetAssetType() + public IObjectGraphType GetAssetType() { - return assetType; + return assetType as IObjectGraphType; } - public IGraphType GetContentType(Guid schemaId) + public IObjectGraphType GetContentType(Guid schemaId) { var schema = schemasById.GetOrDefault(schemaId); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs index c953fc801..d945c3a83 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs @@ -29,9 +29,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL IFieldResolver ResolveContentUrl(ISchemaEntity schema); - IGraphType GetAssetType(); + IObjectGraphType GetAssetType(); - IGraphType GetContentType(Guid schemaId); + IObjectGraphType GetContentType(Guid schemaId); (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index bdb563a2b..128fd9eb5 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var contentType = model.GetContentType(schema.Id); - AddContentFind(schemaId, schemaType, schemaName, contentType); + AddContentFind(schemaType, schemaName, contentType); AddContentQueries(schemaId, schemaType, schemaName, contentType, pageSizeContents); } @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddContentFind(Guid schemaId, string schemaType, string schemaName, IGraphType contentType) + private void AddContentFind(string schemaType, string schemaName, IGraphType contentType) { AddField(new FieldType { @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var contentId = c.GetArgument("id"); - return e.FindContentAsync(schemaId, contentId); + return e.FindContentAsync(contentId); }), Description = $"Find an {schemaName} content by id." }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index eb6ef19f5..c9a63d06b 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -18,13 +18,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentDataGraphType : ObjectGraphType { - public void Initialize(IGraphModel model, ISchemaEntity schema) + public ContentDataGraphType(ISchemaEntity schema) { var schemaType = schema.TypeName(); var schemaName = schema.DisplayName(); Name = $"{schemaType}DataDto"; + Description = $"The structure of the {schemaName} content type."; + } + + public void Initialize(IGraphModel model, ISchemaEntity schema) + { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) { var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); @@ -64,8 +72,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } } - - Description = $"The structure of the {schemaName} content type."; } private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index 63f571c1a..e473627c6 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -15,6 +15,90 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentGraphType : ObjectGraphType { + public ContentGraphType(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) + { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + + Name = $"{schemaType}Dto"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id), + Description = $"The id of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), + Description = $"The version of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), + Description = $"The date and time when the {schemaName} content has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), + Description = $"The user that has created the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), + Description = $"The date and time when the {schemaName} content has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Description = $"The user that has updated the {schemaName} content last." + }); + + AddField(new FieldType + { + Name = "status", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), + Description = $"The the status of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "statusColor", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.StatusColor), + Description = $"The color status of the {schemaName} content." + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Resolver = model.ResolveContentUrl(schema), + Description = $"The url to the the {schemaName} content." + }); + + Interface(); + + Description = $"The structure of a {schemaName} content type."; + } + public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) { var schemaType = schema.TypeName(); @@ -113,6 +197,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } + Interface(); + Description = $"The structure of a {schemaName} content type."; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs new file mode 100644 index 000000000..750879243 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentInterfaceGraphType : InterfaceGraphType + { + public ContentInterfaceGraphType() + { + Name = $"ContentInfaceDto"; + + AddField(new FieldType + { + Name = "id", + ResolvedType = AllTypes.NonNullGuid, + Description = $"The id of the content." + }); + + AddField(new FieldType + { + Name = "version", + ResolvedType = AllTypes.NonNullInt, + Description = $"The version of the content." + }); + + AddField(new FieldType + { + Name = "created", + ResolvedType = AllTypes.NonNullDate, + Description = $"The date and time when the content has been created." + }); + + AddField(new FieldType + { + Name = "createdBy", + ResolvedType = AllTypes.NonNullString, + Description = $"The user that has created the content." + }); + + AddField(new FieldType + { + Name = "lastModified", + ResolvedType = AllTypes.NonNullDate, + Description = $"The date and time when the content has been modified last." + }); + + AddField(new FieldType + { + Name = "lastModifiedBy", + ResolvedType = AllTypes.NonNullString, + Description = $"The user that has updated the content last." + }); + + AddField(new FieldType + { + Name = "status", + ResolvedType = AllTypes.NonNullString, + Description = $"The the status of the content." + }); + + AddField(new FieldType + { + Name = "statusColor", + ResolvedType = AllTypes.NonNullString, + Description = $"The color status of the content." + }); + + AddField(new FieldType + { + Name = "url", + ResolvedType = AllTypes.NonNullString, + Description = $"The url to the the content." + }); + + Description = $"The structure of all content types."; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 8279825fe..42e10034d 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -6,6 +6,8 @@ // ========================================================================== using System; +using System.Collections.Generic; +using System.Linq; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; @@ -19,17 +21,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { private static readonly ValueResolver NoopResolver = (value, c) => value; private readonly ISchemaEntity schema; - private readonly Func schemaResolver; + private readonly Func schemaResolver; + private readonly IDictionary schemaTypes; private readonly IGraphModel model; private readonly IGraphType assetListType; private readonly string fieldName; - public QueryGraphTypeVisitor(ISchemaEntity schema, Func schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName) + public QueryGraphTypeVisitor(ISchemaEntity schema, + IDictionary schemaTypes, + Func schemaResolver, + IGraphModel model, + IGraphType assetListType, + string fieldName) { this.model = model; this.assetListType = assetListType; this.schema = schema; this.schemaResolver = schemaResolver; + this.schemaTypes = schemaTypes; this.fieldName = fieldName; } @@ -112,22 +121,27 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return (assetListType, resolver); } - private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) + private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) { - var schemaId = ((ReferencesFieldProperties)field.RawProperties).SchemaId; - - var contentType = schemaResolver(schemaId); + IGraphType contentType = schemaResolver(field.Properties.SingleId()); if (contentType == null) { - return (null, null); + var union = new ReferenceGraphType(fieldName, schemaTypes, field.Properties.SchemaIds, schemaResolver); + + if (!union.PossibleTypes.Any()) + { + return (null, null); + } + + contentType = union; } var resolver = new ValueResolver((value, c) => { var context = (GraphQLExecutionContext)c.UserContext; - return context.GetReferencedContentsAsync(schemaId, value); + return context.GetReferencedContentsAsync(value); }); var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs new file mode 100644 index 000000000..5de938132 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs @@ -0,0 +1,61 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ReferenceGraphType : UnionGraphType + { + private readonly Dictionary types = new Dictionary(); + + public ReferenceGraphType(string fieldName, IDictionary schemaTypes, IEnumerable schemaIds, Func schemaResolver) + { + Name = $"{fieldName}ReferenceUnionDto"; + + if (schemaIds?.Any() == true) + { + foreach (var schemaId in schemaIds) + { + var schemaType = schemaResolver(schemaId); + + if (schemaType != null) + { + types[schemaId] = schemaType; + } + } + } + else + { + foreach (var schemaType in schemaTypes) + { + types[schemaType.Key.Id] = schemaType.Value; + } + } + + foreach (var type in types) + { + AddPossibleType(type.Value); + } + + ResolveType = value => + { + if (value is IContentEntity content) + { + return types.GetOrDefault(content.Id); + } + + return null; + }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 74db6ab52..743831c79 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (contents.Any()) { - var appVersion = context.App.Version.ToString(); + var appVersion = context.App.Version; var cache = new Dictionary<(Guid, Status), StatusInfo>(); @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries await ResolveCanUpdateAsync(content, result); } - result.CacheDependencies = new HashSet + result.CacheDependencies = new HashSet { appVersion }; @@ -94,8 +94,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries foreach (var content in group) { - content.CacheDependencies.Add(schemaIdentity); - content.CacheDependencies.Add(schemaVersion); + content.CacheDependencies.Add(schema.Id); + content.CacheDependencies.Add(schema.Version); } if (ShouldEnrichWithReferences(context)) @@ -129,12 +129,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries try { - var referencedSchemaId = field.Properties.SchemaId; - var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, referencedSchemaId.ToString()); - - var schemaIdentity = referencedSchema.Id.ToString(); - var schemaVersion = referencedSchema.Version.ToString(); - foreach (var content in contents) { var fieldReference = content.ReferenceData[field.Name]; @@ -151,8 +145,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (referencedContents.Count == 1) { + var reference = referencedContents[0]; + + var referencedSchema = await ContentQuery.GetSchemaOrThrowAsync(context, reference.SchemaId.Id.ToString()); + + content.CacheDependencies.Add(referencedSchema.Id); + content.CacheDependencies.Add(referencedSchema.Version); + var value = - formatted.GetOrAdd(referencedContents[0], + formatted.GetOrAdd(reference, x => x.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig)); fieldReference.AddJsonValue(partitionValue.Key, value); @@ -165,9 +166,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } } - - content.CacheDependencies.Add(schemaIdentity); - content.CacheDependencies.Add(schemaVersion); } } catch (DomainObjectNotFoundException) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 7e68202c9..9f743e85e 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return ids.Select(cachedAssets.GetOrDefault).Where(x => x != null).ToList(); } - public virtual async Task> GetReferencedContentsAsync(Guid schemaId, ICollection ids) + public virtual async Task> GetReferencedContentsAsync(ICollection ids) { Guard.NotNull(ids, nameof(ids)); @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries if (notLoadedContents.Count > 0) { - var result = await contentQuery.QueryAsync(context, schemaId.ToString(), Q.Empty.WithIds(notLoadedContents)); + var result = await contentQuery.QueryAsync(context, notLoadedContents); foreach (var content in result) { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index ca183aad1..5b06f2dc4 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -25,7 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft); - Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + Task> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode filterNode); + + Task> QueryIdsAsync(Guid appId, HashSet ids); Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft); diff --git a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs index 512a2f4c6..1a6b5af96 100644 --- a/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs +++ b/src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs @@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities { public interface IEntityWithCacheDependencies { - HashSet CacheDependencies { get; } + HashSet CacheDependencies { get; } } } \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index bac513d44..fe3b0eb60 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -6,9 +6,11 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using FakeItEasy; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; using Xunit; @@ -181,6 +183,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } + [Fact] + public async Task Should_return_null_single_asset() + { + var assetId = Guid.NewGuid(); + + var query = @" + query { + findAsset(id: """") { + id + } + }".Replace("", assetId.ToString()); + + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findAsset = (object)null + } + }; + + AssertResult(expected, result); + } + [Fact] public async Task Should_return_single_asset_when_finding_asset() { @@ -212,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId))) .Returns(ResultList.CreateFrom(1, asset)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -548,7 +578,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -593,6 +623,34 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } + [Fact] + public async Task Should_return_null_single_content() + { + var contentId = Guid.NewGuid(); + + var query = @" + query { + findMySchemaContent(id: """") { + id + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = (object)null + } + }; + + AssertResult(expected, result); + } + [Fact] public async Task Should_return_single_content_when_finding_content() { @@ -640,7 +698,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -717,7 +775,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() { var contentRefId = Guid.NewGuid(); - var contentRef = CreateContent(contentRefId, Guid.Empty, Guid.Empty); + var contentRef = CreateRefContent(contentRefId, "ref1-field", "ref1"); var contentId = Guid.NewGuid(); var content = CreateContent(contentId, contentRefId, Guid.Empty); @@ -730,16 +788,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL myReferences { iv { id + data { + ref1Field { + iv + } + } } } } } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A.Ignored)) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) .Returns(ResultList.CreateFrom(0, contentRef)); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -759,7 +822,88 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { new { - id = contentRefId + id = contentRefId, + data = new + { + ref1Field = new + { + iv = "ref1" + } + } + } + } + } + } + } + } + }; + + AssertResult(expected, result); + } + + [Fact] + public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() + { + var contentRefId = Guid.NewGuid(); + var contentRef = CreateRefContent(contentRefId, "ref1-field", "ref1"); + + var contentId = Guid.NewGuid(); + var content = CreateContent(contentId, contentRefId, Guid.Empty); + + var query = @" + query { + findMySchemaContent(id: """") { + id + data { + myUnion { + iv { + id + __typename + ...MyRefSchema1Dto { + data { + ref1Field { + iv + } + } + } + } + } + } + } + }".Replace("", contentId.ToString()); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A>.Ignored)) + .Returns(ResultList.CreateFrom(0, contentRef)); + + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) + .Returns(ResultList.CreateFrom(1, content)); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + findMySchemaContent = new + { + id = content.Id, + data = new + { + myReferences = new + { + iv = new[] + { + new + { + id = contentRefId, + __typename = "MyRefSchema1Dto", + data = new + { + ref1Field = new + { + iv = "ref1" + } + }, } } } @@ -794,7 +938,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A.Ignored)) @@ -850,10 +994,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", assetId2.ToString()); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId1))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1))) .Returns(ResultList.CreateFrom(0, asset1)); - A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchId(assetId2))) + A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2))) .Returns(ResultList.CreateFrom(0, asset2)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 }); @@ -909,7 +1053,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -947,7 +1091,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -993,7 +1137,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } }".Replace("", contentId.ToString()); - A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), MatchId(contentId))) + A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId))) .Returns(ResultList.CreateFrom(1, content)); var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); @@ -1012,7 +1156,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); } - private static Q MatchId(Guid contentId) + private static IReadOnlyList MatchId(Guid contentId) + { + return A>.That.Matches(x => x.Count == 1 && x[0] == contentId); + } + + private static Q MatchIdQuery(Guid contentId) { return A.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 51edc65c4..c9e63c7b5 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -40,9 +40,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL protected readonly IContentQueryService contentQuery = A.Fake(); protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None); protected readonly ISchemaEntity schema; + protected readonly ISchemaEntity schemaRef1; + protected readonly ISchemaEntity schemaRef2; protected readonly Context requestContext; protected readonly NamedId appId = NamedId.Of(Guid.NewGuid(), "my-app"); protected readonly NamedId schemaId = NamedId.Of(Guid.NewGuid(), "my-schema"); + protected readonly NamedId schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1"); + protected readonly NamedId schemaRefId2 = NamedId.Of(Guid.NewGuid(), "my-ref-schema2"); protected readonly IGraphQLService sut; public GraphQLTestBase() @@ -50,7 +54,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL app = Mocks.App(appId, Language.DE, Language.GermanGermany); var schemaDef = - new Schema("my-schema") + new Schema(schemaId.Name) + .Publish() .AddJson(1, "my-json", Partitioning.Invariant, new JsonFieldProperties()) .AddString(2, "my-string", Partitioning.Language, @@ -66,7 +71,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .AddDateTime(7, "my-datetime", Partitioning.Invariant, new DateTimeFieldProperties()) .AddReferences(8, "my-references", Partitioning.Invariant, - new ReferencesFieldProperties { SchemaId = schemaId.Id }) + new ReferencesFieldProperties { SchemaId = schemaRefId1.Id }) + .AddReferences(81, "my-union", Partitioning.Invariant, + new ReferencesFieldProperties()) .AddReferences(9, "my-invalid", Partitioning.Invariant, new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }) .AddGeolocation(10, "my-geolocation", Partitioning.Invariant, @@ -79,11 +86,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .AddBoolean(121, "nested-boolean") .AddNumber(122, "nested-number") .AddNumber(123, "nested_number")) - .ConfigureScripts(new SchemaScripts { Query = "" }) - .Publish(); + .ConfigureScripts(new SchemaScripts { Query = "" }); schema = Mocks.Schema(appId, schemaId, schemaDef); + var schemaRef1Def = + new Schema(schemaRefId1.Name) + .Publish() + .AddString(1, "ref1-field", Partitioning.Invariant); + + schemaRef1 = Mocks.Schema(appId, schemaRefId1, schemaRef1Def); + + var schemaRef2Def = + new Schema(schemaRefId2.Name) + .Publish() + .AddString(1, "ref2-field", Partitioning.Invariant); + + schemaRef2 = Mocks.Schema(appId, schemaRefId2, schemaRef2Def); + requestContext = new Context(Mocks.FrontendUser(), app); sut = CreateSut(); @@ -119,6 +139,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL .AddField("my-references", new ContentFieldData() .AddValue("iv", JsonValue.Array(refId.ToString()))) + .AddField("my-union", + new ContentFieldData() + .AddValue("iv", JsonValue.Array(refId.ToString()))) .AddField("my-geolocation", new ContentFieldData() .AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20))) @@ -157,6 +180,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return content; } + protected static IEnrichedContentEntity CreateRefContent(Guid id, string field, string value) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var data = + new NamedContentData() + .AddField(field, + new ContentFieldData() + .AddValue("iv", value)); + + var content = new ContentEntity + { + Id = id, + Version = 1, + Created = now, + CreatedBy = new RefToken(RefTokenType.Subject, "user1"), + LastModified = now, + LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), + Data = data, + DataDraft = data, + Status = Status.Draft, + StatusColor = "red" + }; + + return content; + } + protected static IEnrichedAssetEntity CreateAsset(Guid id) { var now = SystemClock.Instance.GetCurrentInstant(); @@ -207,7 +257,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var appProvider = A.Fake(); A.CallTo(() => appProvider.GetSchemasAsync(appId.Id)) - .Returns(new List { schema }); + .Returns(new List { schema, schemaRef1, schemaRef2 }); var dataLoaderContext = new DataLoaderContextAccessor(); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs index 34def97df..11cefd35b 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs @@ -79,10 +79,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_add_referenced_id_as_dependency() { - var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13); - var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17); - var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23); - var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29); + var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29, refSchemaId2); var source = new IContentEntity[] { @@ -98,20 +98,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var enriched1 = enriched.ElementAt(0); var enriched2 = enriched.ElementAt(1); - Assert.Contains(refSchemaId1.Id.ToString(), enriched1.CacheDependencies); - Assert.Contains(refSchemaId2.Id.ToString(), enriched1.CacheDependencies); + Assert.Contains(refSchemaId1.Id, enriched1.CacheDependencies); + Assert.Contains(refSchemaId2.Id, enriched1.CacheDependencies); - Assert.Contains(refSchemaId1.Id.ToString(), enriched2.CacheDependencies); - Assert.Contains(refSchemaId2.Id.ToString(), enriched2.CacheDependencies); + Assert.Contains(refSchemaId1.Id, enriched2.CacheDependencies); + Assert.Contains(refSchemaId2.Id, enriched2.CacheDependencies); } [Fact] public async Task Should_enrich_with_reference_data() { - var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13); - var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17); - var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23); - var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29); + var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29, refSchemaId2); var source = new IContentEntity[] { @@ -160,10 +160,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries [Fact] public async Task Should_not_enrich_when_content_has_more_items() { - var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13); - var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17); - var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23); - var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29); + var ref1_1 = CreateRefContent(Guid.NewGuid(), "ref1_1", 13, refSchemaId1); + var ref1_2 = CreateRefContent(Guid.NewGuid(), "ref1_2", 17, refSchemaId1); + var ref2_1 = CreateRefContent(Guid.NewGuid(), "ref2_1", 23, refSchemaId2); + var ref2_2 = CreateRefContent(Guid.NewGuid(), "ref2_2", 29, refSchemaId2); var source = new IContentEntity[] { @@ -225,10 +225,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries }; } - private static IEnrichedContentEntity CreateRefContent(Guid id, string name, int number) + private static IEnrichedContentEntity CreateRefContent(Guid id, string name, int number, NamedId schemaId) { return new ContentEntity { + Id = id, DataDraft = new NamedContentData() .AddField("name", @@ -237,7 +238,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries .AddField("number", new ContentFieldData() .AddValue("iv", number)), - Id = id + SchemaId = schemaId }; } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index 0fd177204..b27b61ecf 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -48,10 +48,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var result = await sut.EnrichAsync(source, requestContext); - Assert.Contains(requestContext.App.Version.ToString(), result.CacheDependencies); + Assert.Contains(requestContext.App.Version, result.CacheDependencies); - Assert.Contains(schema.Id.ToString(), result.CacheDependencies); - Assert.Contains(schema.Version.ToString(), result.CacheDependencies); + Assert.Contains(schema.Id, result.CacheDependencies); + Assert.Contains(schema.Version, result.CacheDependencies); } [Fact] From 46d360f7df8b8b2d0f92cef9e24ff9a184b90bd2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 Oct 2019 20:51:35 +0200 Subject: [PATCH 03/14] GraphQL improvements. --- .../Contents/GraphQL/GraphQLModel.cs | 50 +++++---- .../Contents/GraphQL/Types/AssetGraphType.cs | 2 +- .../GraphQL/Types/ContentDataGraphType.cs | 15 +-- .../GraphQL/Types/ContentGraphType.cs | 104 ++++------------- .../Types/ContentInterfaceGraphType.cs | 26 +++-- ...eGraphType.cs => ContentUnionGraphType.cs} | 11 +- .../GraphQL/Types/QueryGraphTypeVisitor.cs | 12 +- .../UserManagerExtensions.cs | 1 - .../Converters/FieldPropertiesDtoFactory.cs | 42 +++---- .../Fields/ReferencesFieldPropertiesDto.cs | 14 ++- .../Contents/GraphQL/GraphQLQueriesTests.cs | 105 ++++++++++++++++-- .../Contents/GraphQL/GraphQLTestBase.cs | 6 +- 12 files changed, 212 insertions(+), 176 deletions(-) rename src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ReferenceGraphType.cs => ContentUnionGraphType.cs} (75%) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index ddb6c9fd8..151264b20 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -28,9 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public sealed class GraphQLModel : IGraphModel { - private readonly Dictionary contentTypes = new Dictionary(); - private readonly Dictionary contentDataTypes = new Dictionary(); - private readonly Dictionary schemasById; + private readonly Dictionary contentTypes = new Dictionary(); private readonly PartitionResolver partitionResolver; private readonly IAppEntity app; private readonly IGraphType assetType; @@ -54,34 +52,47 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL assetType = new AssetGraphType(this); assetListType = new ListGraphType(new NonNullGraphType(assetType)); - schemasById = schemas.Where(x => x.SchemaDef.IsPublished).ToDictionary(x => x.Id); + var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); - graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets); + BuildSchemas(allSchemas); + + graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); graphQLSchema.RegisterValueConverter(JsonConverter.Instance); InitializeContentTypes(); } - private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets) + private void BuildSchemas(List allSchemas) { - var schemas = model.schemasById.Values; - - return new GraphQLSchema { Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) }; + foreach (var schema in allSchemas) + { + contentTypes[schema.Id] = new ContentGraphType(schema); + } } private void InitializeContentTypes() { - foreach (var kvp in contentDataTypes) + foreach (var contentType in contentTypes.Values) { - kvp.Value.Initialize(this, kvp.Key); + contentType.Initialize(this); } - foreach (var kvp in contentTypes) + foreach (var contentType in contentTypes.Values) { - kvp.Value.Initialize(this, kvp.Key, contentDataTypes[kvp.Key]); + graphQLSchema.RegisterType(contentType); } } + private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) + { + var schema = new GraphQLSchema + { + Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) + }; + + return schema; + } + public IFieldResolver ResolveAssetUrl() { var resolver = new FuncFieldResolver(c => @@ -137,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public (IGraphType ResolveType, ValueResolver Resolver) GetGraphType(ISchemaEntity schema, IField field, string fieldName) { - return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, GetContentType, this, assetListType, fieldName)); + return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); } public IObjectGraphType GetAssetType() @@ -147,16 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public IObjectGraphType GetContentType(Guid schemaId) { - var schema = schemasById.GetOrDefault(schemaId); - - if (schema == null) - { - return null; - } - - contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType()); - - return contentTypes.GetOrAdd(schema, s => new ContentGraphType()); + return contentTypes.GetOrDefault(schemaId); } public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index 801f44c81..639ee6d55 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public AssetGraphType(IGraphModel model) { - Name = "AssetDto"; + Name = "Asset"; AddField(new FieldType { diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index c9a63d06b..517776b3f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -18,21 +18,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentDataGraphType : ObjectGraphType { - public ContentDataGraphType(ISchemaEntity schema) + public ContentDataGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); - Name = $"{schemaType}DataDto"; - Description = $"The structure of the {schemaName} content type."; - } - - public void Initialize(IGraphModel model, ISchemaEntity schema) - { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); - foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) { var (resolvedType, valueResolver) = model.GetGraphType(schema, field, fieldName); @@ -72,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } } + + Description = $"The structure of the {schemaName} content type."; } private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index e473627c6..d07ee4b82 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -15,12 +15,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class ContentGraphType : ObjectGraphType { - public ContentGraphType(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) + private readonly ISchemaEntity schema; + private readonly string schemaType; + private readonly string schemaName; + + public ContentGraphType(ISchemaEntity schema) { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); + this.schema = schema; + + schemaType = schema.TypeName(); + schemaName = schema.DisplayName(); - Name = $"{schemaType}Dto"; + Name = $"{schemaType}"; AddField(new FieldType { @@ -86,90 +92,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The color status of the {schemaName} content." }); - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveContentUrl(schema), - Description = $"The url to the the {schemaName} content." - }); - Interface(); Description = $"The structure of a {schemaName} content type."; + + IsTypeOf = CheckType; } - public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) + private bool CheckType(object value) { - var schemaType = schema.TypeName(); - var schemaName = schema.DisplayName(); - - Name = $"{schemaType}Dto"; - - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id), - Description = $"The id of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), - Description = $"The version of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), - Description = $"The date and time when the {schemaName} content has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), - Description = $"The user that has created the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), - Description = $"The date and time when the {schemaName} content has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), - Description = $"The user that has updated the {schemaName} content last." - }); - - AddField(new FieldType - { - Name = "status", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), - Description = $"The the status of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "statusColor", - ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.StatusColor), - Description = $"The color status of the {schemaName} content." - }); + return value is IContentEntity content && content.SchemaId?.Id == schema.Id; + } + public void Initialize(IGraphModel model) + { AddField(new FieldType { Name = "url", @@ -178,6 +114,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The url to the the {schemaName} content." }); + var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); + if (contentDataType.Fields.Any()) { AddField(new FieldType @@ -196,10 +134,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The draft data of the {schemaName} content." }); } - - Interface(); - - Description = $"The structure of a {schemaName} content type."; } private static IFieldResolver Resolve(Func action) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs index 750879243..b1d0c4615 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs @@ -5,20 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using GraphQL.Resolvers; using GraphQL.Types; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public sealed class ContentInterfaceGraphType : InterfaceGraphType + public sealed class ContentInterfaceGraphType : InterfaceGraphType { public ContentInterfaceGraphType() { - Name = $"ContentInfaceDto"; + Name = $"Content"; AddField(new FieldType { Name = "id", ResolvedType = AllTypes.NonNullGuid, + Resolver = Resolve(x => x.Id), Description = $"The id of the content." }); @@ -26,6 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "version", ResolvedType = AllTypes.NonNullInt, + Resolver = Resolve(x => x.Version), Description = $"The version of the content." }); @@ -33,6 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "created", ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.Created), Description = $"The date and time when the content has been created." }); @@ -40,6 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "createdBy", ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.CreatedBy.ToString()), Description = $"The user that has created the content." }); @@ -47,6 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModified", ResolvedType = AllTypes.NonNullDate, + Resolver = Resolve(x => x.LastModified), Description = $"The date and time when the content has been modified last." }); @@ -54,6 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModifiedBy", ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.LastModifiedBy.ToString()), Description = $"The user that has updated the content last." }); @@ -61,6 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "status", ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), Description = $"The the status of the content." }); @@ -68,17 +77,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "statusColor", ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.StatusColor), Description = $"The color status of the content." }); - AddField(new FieldType - { - Name = "url", - ResolvedType = AllTypes.NonNullString, - Description = $"The url to the the content." - }); - Description = $"The structure of all content types."; } + + private static IFieldResolver Resolve(Func action) + { + return new FuncFieldResolver(c => action(c.Source)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs similarity index 75% rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs rename to src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs index 5de938132..523b58032 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs @@ -9,16 +9,15 @@ using System; using System.Collections.Generic; using System.Linq; using GraphQL.Types; -using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public sealed class ReferenceGraphType : UnionGraphType + public sealed class ContentUnionGraphType : UnionGraphType { private readonly Dictionary types = new Dictionary(); - public ReferenceGraphType(string fieldName, IDictionary schemaTypes, IEnumerable schemaIds, Func schemaResolver) + public ContentUnionGraphType(string fieldName, Dictionary schemaTypes, IEnumerable schemaIds) { Name = $"{fieldName}ReferenceUnionDto"; @@ -26,7 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { foreach (var schemaId in schemaIds) { - var schemaType = schemaResolver(schemaId); + var schemaType = schemaTypes.GetOrDefault(schemaId); if (schemaType != null) { @@ -38,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { foreach (var schemaType in schemaTypes) { - types[schemaType.Key.Id] = schemaType.Value; + types[schemaType.Key] = schemaType.Value; } } @@ -51,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { if (value is IContentEntity content) { - return types.GetOrDefault(content.Id); + return types.GetOrDefault(content.SchemaId.Id); } return null; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 42e10034d..e038b0432 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -11,6 +11,7 @@ using System.Linq; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types @@ -20,16 +21,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType ResolveType, ValueResolver Resolver)> { private static readonly ValueResolver NoopResolver = (value, c) => value; + private readonly Dictionary schemaTypes; private readonly ISchemaEntity schema; - private readonly Func schemaResolver; - private readonly IDictionary schemaTypes; private readonly IGraphModel model; private readonly IGraphType assetListType; private readonly string fieldName; public QueryGraphTypeVisitor(ISchemaEntity schema, - IDictionary schemaTypes, - Func schemaResolver, + Dictionary schemaTypes, IGraphModel model, IGraphType assetListType, string fieldName) @@ -37,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types this.model = model; this.assetListType = assetListType; this.schema = schema; - this.schemaResolver = schemaResolver; this.schemaTypes = schemaTypes; this.fieldName = fieldName; } @@ -123,11 +121,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private (IGraphType ResolveType, ValueResolver Resolver) ResolveReferences(IField field) { - IGraphType contentType = schemaResolver(field.Properties.SingleId()); + IGraphType contentType = schemaTypes.GetOrDefault(field.Properties.SingleId()); if (contentType == null) { - var union = new ReferenceGraphType(fieldName, schemaTypes, field.Properties.SchemaIds, schemaResolver); + var union = new ContentUnionGraphType(fieldName, schemaTypes, field.Properties.SchemaIds); if (!union.PossibleTypes.Any()) { diff --git a/src/Squidex.Domain.Users/UserManagerExtensions.cs b/src/Squidex.Domain.Users/UserManagerExtensions.cs index e89160674..880663a92 100644 --- a/src/Squidex.Domain.Users/UserManagerExtensions.cs +++ b/src/Squidex.Domain.Users/UserManagerExtensions.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Security.Claims; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index c2cacdb89..9909d8435 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -30,6 +30,15 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters return SimpleMapper.Map(properties, new ArrayFieldPropertiesDto()); } + public FieldPropertiesDto Visit(AssetsFieldProperties properties) + { + var result = SimpleMapper.Map(properties, new AssetsFieldPropertiesDto()); + + result.AllowedExtensions = properties.AllowedExtensions?.ToArray(); + + return result; + } + public FieldPropertiesDto Visit(BooleanFieldProperties properties) { return SimpleMapper.Map(properties, new BooleanFieldPropertiesDto()); @@ -50,50 +59,45 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters return SimpleMapper.Map(properties, new JsonFieldPropertiesDto()); } - public FieldPropertiesDto Visit(ReferencesFieldProperties properties) - { - return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(UIFieldProperties properties) - { - return SimpleMapper.Map(properties, new UIFieldPropertiesDto()); - } - - public FieldPropertiesDto Visit(TagsFieldProperties properties) + public FieldPropertiesDto Visit(NumberFieldProperties properties) { - var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); + var result = SimpleMapper.Map(properties, new NumberFieldPropertiesDto()); result.AllowedValues = properties.AllowedValues?.ToArray(); return result; } - public FieldPropertiesDto Visit(AssetsFieldProperties properties) + public FieldPropertiesDto Visit(ReferencesFieldProperties properties) { - var result = SimpleMapper.Map(properties, new AssetsFieldPropertiesDto()); + var result = SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto()); - result.AllowedExtensions = properties.AllowedExtensions?.ToArray(); + result.SchemaIds = properties.SchemaIds?.ToArray(); return result; } - public FieldPropertiesDto Visit(NumberFieldProperties properties) + public FieldPropertiesDto Visit(StringFieldProperties properties) { - var result = SimpleMapper.Map(properties, new NumberFieldPropertiesDto()); + var result = SimpleMapper.Map(properties, new StringFieldPropertiesDto()); result.AllowedValues = properties.AllowedValues?.ToArray(); return result; } - public FieldPropertiesDto Visit(StringFieldProperties properties) + public FieldPropertiesDto Visit(TagsFieldProperties properties) { - var result = SimpleMapper.Map(properties, new StringFieldPropertiesDto()); + var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); result.AllowedValues = properties.AllowedValues?.ToArray(); return result; } + + public FieldPropertiesDto Visit(UIFieldProperties properties) + { + return SimpleMapper.Map(properties, new UIFieldPropertiesDto()); + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs index 8f144931c..d85708d51 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ReferencesFieldPropertiesDto.cs @@ -7,6 +7,7 @@ using System; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields @@ -39,13 +40,20 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields public ReferencesFieldEditor Editor { get; set; } /// - /// The id of the referenced schema. + /// The id of the referenced schemas. /// - public Guid SchemaId { get; set; } + public Guid[] SchemaIds { get; set; } public override FieldProperties ToProperties() { - return SimpleMapper.Map(this, new ReferencesFieldProperties()); + var result = SimpleMapper.Map(this, new ReferencesFieldProperties()); + + if (SchemaIds != null) + { + result.SchemaIds = ReadOnlyCollection.Create(SchemaIds); + } + + return result; } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index fe3b0eb60..78c2ad0c8 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs @@ -18,6 +18,95 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public class GraphQLQueriesTests : GraphQLTestBase { + [Fact] + public async Task Should_introspect() + { + const string query = @" + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + }"; + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); + + var json = serializer.Serialize(result.Response, true); + + Assert.NotEmpty(json); + } + [Theory] [InlineData(null)] [InlineData("")] @@ -775,7 +864,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() { var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(contentRefId, "ref1-field", "ref1"); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); var contentId = Guid.NewGuid(); var content = CreateContent(contentId, contentRefId, Guid.Empty); @@ -845,7 +934,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public async Task Should_also_fetch_union_contents_when_field_is_included_in_query() { var contentRefId = Guid.NewGuid(); - var contentRef = CreateRefContent(contentRefId, "ref1-field", "ref1"); + var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1"); var contentId = Guid.NewGuid(); var content = CreateContent(contentId, contentRefId, Guid.Empty); @@ -857,15 +946,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data { myUnion { iv { - id - __typename - ...MyRefSchema1Dto { + ... on Content { + id + } + ... on MyRefSchema1 { data { ref1Field { iv } } } + __typename } } } @@ -889,14 +980,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL id = content.Id, data = new { - myReferences = new + myUnion = new { iv = new[] { new { id = contentRefId, - __typename = "MyRefSchema1Dto", data = new { ref1Field = new @@ -904,6 +994,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL iv = "ref1" } }, + __typename = "MyRefSchema1" } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index c9e63c7b5..e5eb10df8 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL sut = CreateSut(); } - protected static IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) + protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null, NamedContentData dataDraft = null) { var now = SystemClock.Instance.GetCurrentInstant(); @@ -173,6 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), Data = data, DataDraft = dataDraft, + SchemaId = schemaId, Status = Status.Draft, StatusColor = "red" }; @@ -180,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return content; } - protected static IEnrichedContentEntity CreateRefContent(Guid id, string field, string value) + protected static IEnrichedContentEntity CreateRefContent(NamedId schemaId, Guid id, string field, string value) { var now = SystemClock.Instance.GetCurrentInstant(); @@ -200,6 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"), Data = data, DataDraft = data, + SchemaId = schemaId, Status = Status.Draft, StatusColor = "red" }; From e455d2f7cc559a84e04d60668c6f41134ea9674e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 Oct 2019 23:33:34 +0200 Subject: [PATCH 04/14] UI updated --- .../Schemas/SchemaExtensions.cs | 19 +++ .../Contents/ContentEntity.cs | 7 + .../Contents/IEnrichedContentEntity.cs | 7 + .../Contents/Queries/ContentEnricher.cs | 61 +++++--- .../Controllers/Contents/Models/ContentDto.cs | 22 +++ .../Contents/Models/ContentsDto.cs | 9 +- .../Controllers/Schemas/Models/FieldDto.cs | 44 ++++++ .../Schemas/Models/SchemaDetailsDto.cs | 33 +--- .../app/features/content/declarations.ts | 2 + src/Squidex/app/features/content/module.ts | 6 +- .../shared/content-selector-item.component.ts | 100 ++++++++++++ .../shared/content-value-editor.component.ts | 10 +- .../content/shared/content.component.html | 2 +- .../content/shared/content.component.ts | 2 +- .../shared/contents-selector.component.html | 148 ++++++++++-------- .../shared/contents-selector.component.scss | 11 ++ .../shared/contents-selector.component.ts | 45 ++++-- .../shared/field-editor.component.html | 34 ++-- .../shared/reference-item.component.scss | 24 +++ .../shared/reference-item.component.ts | 104 ++++++++++++ .../shared/references-editor.component.html | 18 +-- .../shared/references-editor.component.ts | 71 ++++----- .../references-validation.component.html | 8 +- .../types/references-validation.component.ts | 12 +- .../pages/workflows/workflow.component.ts | 3 +- .../workflows/workflows-page.component.ts | 11 +- .../references-dropdown.component.ts | 4 +- src/Squidex/app/shared/internal.ts | 1 + src/Squidex/app/shared/module.ts | 2 + .../shared/services/contents.service.spec.ts | 6 + .../app/shared/services/contents.service.ts | 20 +++ .../app/shared/services/schemas.service.ts | 80 +++++----- .../app/shared/services/schemas.types.ts | 6 +- .../app/shared/state/contents.state.ts | 4 + src/Squidex/app/shared/state/query.ts | 2 +- .../state}/schema-tag-converter.ts | 16 +- src/Squidex/package-lock.json | 4 +- 37 files changed, 678 insertions(+), 280 deletions(-) create mode 100644 src/Squidex/app/features/content/shared/content-selector-item.component.ts create mode 100644 src/Squidex/app/features/content/shared/reference-item.component.scss create mode 100644 src/Squidex/app/features/content/shared/reference-item.component.ts rename src/Squidex/app/{features/settings/pages/workflows => shared/state}/schema-tag-converter.ts (82%) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs index 333ad6d05..3ecfe0747 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs @@ -59,6 +59,25 @@ namespace Squidex.Domain.Apps.Core.Schemas return properties.SchemaIds?.Count == 1 ? properties.SchemaIds[0] : Guid.Empty; } + public static IEnumerable ReferenceFields(this Schema schema) + { + var references = schema.Fields.Where(x => x.RawProperties.IsReferenceField); + + if (references.Any()) + { + return references; + } + + references = schema.Fields.Where(x => x.RawProperties.IsListField); + + if (references.Any()) + { + return references; + } + + return schema.Fields.Take(1); + } + public static IEnumerable> ResolvingReferences(this Schema schema) { return schema.Fields.OfType>() diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index e5f9c4647..974094d57 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using NodaTime; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents @@ -45,6 +46,12 @@ namespace Squidex.Domain.Apps.Entities.Contents public string StatusColor { get; set; } + public string SchemaName { get; set; } + + public string SchemaDisplayName { get; set; } + + public RootField[] ReferenceFields { get; set; } + public bool CanUpdate { get; set; } public bool IsPending { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs index 66d125dd9..b8e45f2eb 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; namespace Squidex.Domain.Apps.Entities.Contents { @@ -15,6 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents string StatusColor { get; } + string SchemaName { get; } + + string SchemaDisplayName { get; } + + RootField[] ReferenceFields { get; } + StatusInfo[] Nexts { get; } NamedContentData ReferenceData { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs index 743831c79..b262976c3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs @@ -69,18 +69,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var result = SimpleMapper.Map(content, new ContentEntity()); - await ResolveColorAsync(content, result, cache); - - if (ShouldEnrichWithStatuses(context)) + if (ShouldEnrich(context)) { - await ResolveNextsAsync(content, result, context); - await ResolveCanUpdateAsync(content, result); - } + await ResolveColorAsync(content, result, cache); - result.CacheDependencies = new HashSet - { - appVersion - }; + if (ShouldEnrichWithStatuses(context)) + { + await ResolveNextsAsync(content, result, context); + await ResolveCanUpdateAsync(content, result); + } + } results.Add(result); } @@ -89,16 +87,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var schema = await ContentQuery.GetSchemaOrThrowAsync(context, group.Key.ToString()); - var schemaIdentity = schema.Id.ToString(); - var schemaVersion = schema.Version.ToString(); - foreach (var content in group) { - content.CacheDependencies.Add(schema.Id); - content.CacheDependencies.Add(schema.Version); + content.CacheDependencies = new HashSet + { + schema.Id, + schema.Version + }; } - if (ShouldEnrichWithReferences(context)) + if (ShouldEnrichWithSchema(context)) + { + var referenceFields = schema.SchemaDef.ReferenceFields().ToArray(); + + var schemaName = schema.SchemaDef.Name; + var schemaDisplayName = schema.DisplayName(); + + foreach (var content in group) + { + content.ReferenceFields = referenceFields; + content.SchemaName = schemaName; + content.SchemaDisplayName = schemaDisplayName; + } + } + + if (ShouldEnrich(context)) { await ResolveReferencesAsync(schema, group, context); } @@ -152,9 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries content.CacheDependencies.Add(referencedSchema.Id); content.CacheDependencies.Add(referencedSchema.Version); - var value = - formatted.GetOrAdd(reference, - x => x.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig)); + var value = formatted.GetOrAdd(reference, x => Format(x, context, referencedSchema)); fieldReference.AddJsonValue(partitionValue.Key, value); } @@ -175,6 +186,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } } + private static JsonObject Format(IContentEntity content, Context context, ISchemaEntity referencedSchema) + { + return content.DataDraft.FormatReferences(referencedSchema.SchemaDef, context.App.LanguagesConfig); + } + private static JsonObject CreateFallback(Context context, List referencedContents) { var text = $"{referencedContents.Count} Reference(s)"; @@ -242,12 +258,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return info.Color; } + private static bool ShouldEnrichWithSchema(Context context) + { + return context.IsFrontendClient; + } + private static bool ShouldEnrichWithStatuses(Context context) { return context.IsFrontendClient || context.IsResolveFlow(); } - private static bool ShouldEnrichWithReferences(Context context) + private static bool ShouldEnrich(Context context) { return context.IsFrontendClient && !context.IsNoEnrichment(); } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index dede3c34d..8399e0edd 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -7,7 +7,9 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Linq; using NodaTime; +using Squidex.Areas.Api.Controllers.Schemas.Models; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities; @@ -84,6 +86,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// public string StatusColor { get; set; } + /// + /// The name of the schema. + /// + public string SchemaName { get; set; } + + /// + /// The display name of the schema. + /// + public string SchemaDisplayName { get; set; } + + /// + /// The reference fields. + /// + public FieldDto[] ReferenceFields { get; set; } + /// /// The version of the content. /// @@ -104,6 +121,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models response.DataDraft = content.DataDraft; } + if (content.ReferenceFields != null) + { + response.ReferenceFields = content.ReferenceFields.Select(FieldDto.FromField).ToArray(); + } + if (content.ScheduleJob != null) { response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index 83ed3a14b..73b9d976a 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -44,9 +44,14 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() }; - await result.AssignStatusesAsync(workflow, schema); + if (schema != null) + { + await result.AssignStatusesAsync(workflow, schema); + + result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + } - return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); + return result; } private async Task AssignStatusesAsync(IContentWorkflow workflow, ISchemaEntity schema) diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs index bc583bb03..b222fc132 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldDto.cs @@ -7,7 +7,10 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; using Squidex.Areas.Api.Controllers.Schemas.Models.Fields; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Reflection; using Squidex.Web; namespace Squidex.Areas.Api.Controllers.Schemas.Models @@ -58,6 +61,47 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models /// public List Nested { get; set; } + public static NestedFieldDto FromField(NestedField field) + { + var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); + + var result = + SimpleMapper.Map(field, + new NestedFieldDto + { + FieldId = field.Id, + Properties = properties + }); + + return result; + } + + public static FieldDto FromField(RootField field) + { + var properties = FieldPropertiesDtoFactory.Create(field.RawProperties); + + var result = + SimpleMapper.Map(field, + new FieldDto + { + FieldId = field.Id, + Properties = properties, + Partitioning = field.Partitioning.Key + }); + + if (field is IArrayField arrayField) + { + result.Nested = new List(); + + foreach (var nestedField in arrayField.Fields) + { + result.Nested.Add(FromField(nestedField)); + } + } + + return result; + } + public void CreateLinks(ApiController controller, string app, string schema, bool allowUpdate) { allowUpdate = allowUpdate && !IsLocked; diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs index 716dc5ad0..d2b1402a3 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs @@ -7,8 +7,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using Squidex.Areas.Api.Controllers.Schemas.Models.Converters; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.Reflection; using Squidex.Shared; @@ -54,36 +52,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models foreach (var field in schema.SchemaDef.Fields) { - var fieldPropertiesDto = FieldPropertiesDtoFactory.Create(field.RawProperties); - var fieldDto = - SimpleMapper.Map(field, - new FieldDto - { - FieldId = field.Id, - Properties = fieldPropertiesDto, - Partitioning = field.Partitioning.Key - }); - - if (field is IArrayField arrayField) - { - fieldDto.Nested = new List(); - - foreach (var nestedField in arrayField.Fields) - { - var nestedFieldPropertiesDto = FieldPropertiesDtoFactory.Create(nestedField.RawProperties); - var nestedFieldDto = - SimpleMapper.Map(nestedField, - new NestedFieldDto - { - FieldId = nestedField.Id, - Properties = nestedFieldPropertiesDto - }); - - fieldDto.Nested.Add(nestedFieldDto); - } - } - - result.Fields.Add(fieldDto); + result.Fields.Add(FieldDto.FromField(field)); } result.CreateLinks(controller, app); diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index e6ac799a9..952e04938 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/src/Squidex/app/features/content/declarations.ts @@ -21,8 +21,10 @@ export * from './shared/content.component'; export * from './shared/content-status.component'; export * from './shared/content-value.component'; export * from './shared/content-value-editor.component'; +export * from './shared/content-selector-item.component'; export * from './shared/contents-selector.component'; export * from './shared/due-time-selector.component'; export * from './shared/field-editor.component'; export * from './shared/preview-button.component'; +export * from './shared/reference-item.component'; export * from './shared/references-editor.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index e22b6b3c3..4fef616d1 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -29,6 +29,7 @@ import { ContentFieldComponent, ContentHistoryPageComponent, ContentPageComponent, + ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentsSelectorComponent, @@ -39,6 +40,7 @@ import { FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, + ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent } from './declarations'; @@ -110,10 +112,11 @@ const routes: Routes = [ ArrayItemComponent, AssetsEditorComponent, CommentsPageComponent, + ContentComponent, ContentFieldComponent, ContentHistoryPageComponent, - ContentComponent, ContentPageComponent, + ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentsSelectorComponent, @@ -124,6 +127,7 @@ const routes: Routes = [ FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, + ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent ] diff --git a/src/Squidex/app/features/content/shared/content-selector-item.component.ts b/src/Squidex/app/features/content/shared/content-selector-item.component.ts new file mode 100644 index 000000000..c12213f1e --- /dev/null +++ b/src/Squidex/app/features/content/shared/content-selector-item.component.ts @@ -0,0 +1,100 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; + +import { + AppLanguageDto, + ContentDto, + getContentValue, + RootFieldDto +} from '@app/shared'; + +/* tslint:disable:component-selector */ + +@Component({ + selector: '[sqxContentSelectorItem]', + template: ` + + + + + + + + + + + + + + + + + + {{content.lastModified | sqxFromNow}} + + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ContentSelectorItemComponent implements OnChanges { + @Output() + public selectedChange = new EventEmitter(); + + @Input() + public selected = false; + + @Input() + public selectable = true; + + @Input() + public language: AppLanguageDto; + + @Input() + public fields: RootFieldDto[]; + + @Input('sqxContentSelectorItem') + public content: ContentDto; + + public values: any[] = []; + + public ngOnChanges(changes: SimpleChanges) { + if (changes['content'] || changes['language']) { + this.updateValues(); + } + } + + public toggle() { + if (this.selectable) { + this.emitSelectedChange(!this.selected); + } + } + + public emitSelectedChange(isSelected: boolean) { + this.selectedChange.emit(isSelected); + } + + private updateValues() { + this.values = []; + + for (let field of this.fields) { + const { formatted } = getContentValue(this.content, this.language, field); + + this.values.push(formatted); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-value-editor.component.ts b/src/Squidex/app/features/content/shared/content-value-editor.component.ts index bdd6cd7b8..16aa91ace 100644 --- a/src/Squidex/app/features/content/shared/content-value-editor.component.ts +++ b/src/Squidex/app/features/content/shared/content-value-editor.component.ts @@ -16,20 +16,20 @@ import { FieldDto } from '@app/shared';
- + - + @@ -39,13 +39,13 @@ import { FieldDto } from '@app/shared'; - + diff --git a/src/Squidex/app/features/content/shared/content.component.html b/src/Squidex/app/features/content/shared/content.component.html index 509a397f6..df881b423 100644 --- a/src/Squidex/app/features/content/shared/content.component.html +++ b/src/Squidex/app/features/content/shared/content.component.html @@ -1,4 +1,4 @@ - + - Select contents +
+
+ +
+
-
- -
-
- - -
- -
- -
+ +
+ +
+
+ + +
+ +
+ +
+
-
- - - - - - - - - -
- - - - - - - - - -
-
- -
-
- - - + +
+
+ + + + + + + +
+ + + + + + + + + +
-
- +
+
+ + + +
+
+
+ + +
diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.scss b/src/Squidex/app/features/content/shared/contents-selector.component.scss index 0d2c43170..dc1189cf8 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.scss +++ b/src/Squidex/app/features/content/shared/contents-selector.component.scss @@ -11,6 +11,17 @@ } } +.col-selector { + position: relative; + + .form-control { + @include absolute(-.4rem, auto, auto, 0); + max-width: 300px; + min-width: 300px; + color: $color-dark-foreground; + } +} + .table-container { display: inline-block; padding: 0; diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.ts b/src/Squidex/app/features/content/shared/contents-selector.component.ts index 02a97c5f9..b3e3e45e2 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.ts +++ b/src/Squidex/app/features/content/shared/contents-selector.component.ts @@ -15,7 +15,10 @@ import { QueryModel, queryModelFromSchema, ResourceOwner, - SchemaDetailsDto + SchemaDetailsDto, + SchemaDto, + SchemasState, + Types } from '@app/shared'; @Component({ @@ -23,7 +26,8 @@ import { styleUrls: ['./contents-selector.component.scss'], templateUrl: './contents-selector.component.html', providers: [ - ManualContentsState + ManualContentsState, + SchemasState ] }) export class ContentsSelectorComponent extends ResourceOwner implements OnInit { @@ -42,7 +46,6 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit { @Input() public alreadySelected: ContentDto[]; - @Input() public schema: SchemaDetailsDto; public queryModel: QueryModel; @@ -54,22 +57,44 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit { public minWidth: string; constructor( - public readonly contentsState: ManualContentsState + public readonly contentsState: ManualContentsState, + public readonly schemasState: SchemasState ) { super(); } public ngOnInit() { - this.minWidth = `${200 + (200 * this.schema.referenceFields.length)}px`; - this.own( this.contentsState.statuses .subscribe(() => { this.updateModel(); })); - this.contentsState.schema = this.schema; - this.contentsState.load(); + this.own( + this.schemasState.selectedSchema + .subscribe(schema => { + this.schema = schema; + + this.minWidth = `${200 + (200 * schema.referenceFields.length)}px`; + + this.contentsState.schema = schema; + this.contentsState.load(); + + this.updateModel(); + })); + + this.schemasState.load() + .subscribe(() => { + this.selectSchema(this.schemasState.snapshot.schemas.at(0)); + }); + } + + public selectSchema(selected: string | SchemaDto) { + if (Types.is(selected, SchemaDto)) { + this.schemasState.select(selected.id).subscribe(); + } else { + this.schemasState.select(selected).subscribe(); + } } public reload() { @@ -136,7 +161,9 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit { } private updateModel() { - this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses); + if (this.schema) { + this.queryModel = queryModelFromSchema(this.schema, this.languages, this.contentsState.snapshot.statuses); + } } public trackByContent(content: ContentDto): string { diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 3d81911cf..1b89955ae 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -34,7 +34,7 @@ - + @@ -44,7 +44,7 @@ - + @@ -53,21 +53,21 @@ - + - + -
+