diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs index 4316b0d0c..45cbd8fd7 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmSchemaExtensions.cs @@ -5,14 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using System.Linq; using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.GenerateEdmSchema { + public delegate (EdmComplexType Type, bool Created) EdmTypeFactory(string names); + public static class EdmSchemaExtensions { public static string EscapeEdmField(this string field) @@ -25,30 +25,39 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema return field.Replace("_", "-"); } - public static EdmComplexType BuildEdmType(this Schema schema, PartitionResolver partitionResolver, Func typeResolver) + public static EdmComplexType BuildEdmType(this Schema schema, bool withHidden, PartitionResolver partitionResolver, EdmTypeFactory typeFactory) { - Guard.NotNull(typeResolver, nameof(typeResolver)); + Guard.NotNull(typeFactory, nameof(typeFactory)); Guard.NotNull(partitionResolver, nameof(partitionResolver)); - var schemaName = schema.Name.ToPascalCase(); + var (edmType, _) = typeFactory("Data"); - var edmType = new EdmComplexType("Squidex", schemaName); + var visitor = new EdmTypeVisitor(typeFactory); - foreach (var field in schema.FieldsByName.Values.Where(x => !x.IsHidden)) + foreach (var field in schema.FieldsByName.Values) { - var edmValueType = EdmTypeVisitor.CreateEdmType(field); + if (!withHidden && field.IsHidden) + { + continue; + } + + var fieldEdmType = field.Accept(visitor); - if (edmValueType == null) + if (fieldEdmType == null) { continue; } - var partitionType = typeResolver(new EdmComplexType("Squidex", $"{schemaName}{field.Name.ToPascalCase()}Property")); - var partition = partitionResolver(field.Partitioning); + var (partitionType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}"); - foreach (var partitionItem in partition) + if (created) { - partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), edmValueType); + var partition = partitionResolver(field.Partitioning); + + foreach (var partitionItem in partition) + { + partitionType.AddStructuralProperty(partitionItem.Key.EscapeEdmField(), fieldEdmType); + } } edmType.AddStructuralProperty(field.Name.EscapeEdmField(), new EdmComplexTypeReference(partitionType, false)); diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs index 789da3081..388448792 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs @@ -7,25 +7,42 @@ using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Core.GenerateEdmSchema { public sealed class EdmTypeVisitor : IFieldVisitor { - private static readonly EdmTypeVisitor Instance = new EdmTypeVisitor(); + private readonly EdmTypeFactory typeFactory; - private EdmTypeVisitor() + internal EdmTypeVisitor(EdmTypeFactory typeFactory) { + this.typeFactory = typeFactory; } - public static IEdmTypeReference CreateEdmType(IField field) + public IEdmTypeReference CreateEdmType(IField field) { - return field.Accept(Instance); + return field.Accept(this); } public IEdmTypeReference Visit(IArrayField field) { - return null; + var (fieldEdmType, created) = typeFactory($"Data.{field.Name.ToPascalCase()}.Item"); + + if (created) + { + foreach (var nestedField in field.Fields) + { + var nestedEdmType = nestedField.Accept(this); + + if (nestedEdmType != null) + { + fieldEdmType.AddStructuralProperty(nestedField.Name.EscapeEdmField(), nestedEdmType); + } + } + } + + return new EdmComplexTypeReference(fieldEdmType, false); } public IEdmTypeReference Visit(IField field) diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs index 4fab6e41e..2b73e222b 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs @@ -65,6 +65,18 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors } result[1] = field.Id.ToString(); + + if (field is IArrayField arrayField && result.Count > 3) + { + var nestedEdmName = result[3].UnescapeEdmField(); + + if (!arrayField.FieldsByName.TryGetValue(nestedEdmName, out var nestedField)) + { + throw new NotSupportedException(); + } + + result[3] = nestedField.Id.ToString(); + } } if (result.Count > 2) diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index c4840e906..6d3b16f17 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -262,7 +262,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { try { - var model = modelBuilder.BuildEdmModel(schema, context.App); + var model = modelBuilder.BuildEdmModel(context.App, schema, context.IsFrontendClient); var result = model.ParseQuery(query).ToQuery(); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs index f891b4906..28b76917f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Linq; using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core; @@ -26,34 +27,56 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm { } - public virtual IEdmModel BuildEdmModel(ISchemaEntity schema, IAppEntity app) + public virtual IEdmModel BuildEdmModel(IAppEntity app, ISchemaEntity schema, bool withHidden) { Guard.NotNull(schema, nameof(schema)); - var cacheKey = $"{schema.Id}_{schema.Version}_{app.Id}_{app.Version}"; + var cacheKey = BuildCacheKey(app, schema, withHidden); var result = Cache.GetOrCreate(cacheKey, entry => { entry.AbsoluteExpirationRelativeToNow = CacheTime; - return BuildEdmModel(schema.SchemaDef, app.PartitionResolver()); + return BuildEdmModel(schema.SchemaDef, app, withHidden); }); return result; } - private static EdmModel BuildEdmModel(Schema schema, PartitionResolver partitionResolver) + private static EdmModel BuildEdmModel(Schema schema, IAppEntity app, bool withHidden) { var model = new EdmModel(); - var schemaType = schema.BuildEdmType(partitionResolver, x => + var pascalAppName = app.Name.ToPascalCase(); + var pascalSchemaName = schema.Name.ToPascalCase(); + + var typeFactory = new EdmTypeFactory(name => { - model.AddElement(x); + var finalName = pascalSchemaName; + + if (!string.IsNullOrWhiteSpace(name)) + { + finalName += "."; + finalName += name; + } + + var result = model.SchemaElements.OfType().FirstOrDefault(x => x.Name == finalName); + + if (result != null) + { + return (result, false); + } + + result = new EdmComplexType(pascalAppName, finalName); - return x; + model.AddElement(result); + + return (result, true); }); - var entityType = new EdmEntityType("Squidex", schema.Name); + var schemaType = schema.BuildEdmType(withHidden, app.PartitionResolver(), typeFactory); + + var entityType = new EdmEntityType(app.Name.ToPascalCase(), schema.Name); entityType.AddStructuralProperty(nameof(IContentEntity.Id).ToCamelCase(), EdmPrimitiveTypeKind.String); entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset); entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String); @@ -73,5 +96,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm return model; } + + private static string BuildCacheKey(IAppEntity app, ISchemaEntity schema, bool withHidden) + { + return string.Join("_", schema.Id, schema.Version, app.Id, app.Version, withHidden); + } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs index 92ab7b60d..b13fb25ff 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateEdmSchema/EdmTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Microsoft.OData.Edm; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.GenerateEdmSchema; using Squidex.Infrastructure; @@ -31,7 +32,12 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateEdmSchema { var languagesConfig = LanguagesConfig.Build(Language.DE, Language.EN); - var edmModel = TestUtils.MixedSchema().BuildEdmType(languagesConfig.ToResolver(), x => x); + var typeFactory = new EdmTypeFactory(names => + { + return (new EdmComplexType("Squidex", string.Join(".", names)), true); + }); + + var edmModel = TestUtils.MixedSchema().BuildEdmType(true, languagesConfig.ToResolver(), typeFactory); Assert.NotNull(edmModel); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs index cbf1b5d0b..a27b6b0ad 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs @@ -310,7 +310,7 @@ namespace Squidex.Domain.Apps.Entities.Contents SetupClaims(); SetupSchema(); - A.CallTo(() => modelBuilder.BuildEdmModel(schema, app)) + A.CallTo(() => modelBuilder.BuildEdmModel(app, schema, A.Ignored)) .Throws(new ODataException()); return Assert.ThrowsAsync(() => sut.QueryAsync(context, schemaId.Name, Q.Empty.WithODataQuery("query"))); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs index 3b4056167..746dc12ae 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/MongoDbQueryTests.cs @@ -60,6 +60,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb new ReferencesFieldProperties()) .AddString(8, "dashed-field", Partitioning.Invariant, new StringFieldProperties()) + .AddArray(9, "hobbies", Partitioning.Invariant, a => a + .AddString(91, "name")) .Update(new SchemaProperties()); var schema = A.Dummy(); @@ -178,6 +180,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb Assert.Equal(o, i); } + [Fact] + public void Should_make_query_with_array_field() + { + var i = F(FilterBuilder.Eq("data/hobbies/iv/name", "PC")); + var o = C("{ 'do.9.iv.91' : 'PC' }"); + + Assert.Equal(o, i); + } + [Fact] public void Should_make_query_with_assets_equals() {