Browse Source

Started with GraphQL.

pull/422/head
Sebastian 6 years ago
parent
commit
6035db8fc0
  1. 15
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs
  2. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  4. 12
      src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  5. 14
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  6. 8
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  7. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  8. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  9. 12
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  10. 86
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  11. 84
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs
  12. 30
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  13. 61
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ReferenceGraphType.cs
  14. 26
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  15. 4
      src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  16. 4
      src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  17. 2
      src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs
  18. 177
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  19. 60
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  20. 37
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherReferencesTests.cs
  21. 6
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs

15
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollection.cs

@ -169,15 +169,24 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
});
}
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> filterNode)
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(ISchemaEntity schema, FilterNode<ClrValue> 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<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(HashSet<Guid> 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<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId)

10
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
@ -124,6 +124,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await contents.QueryIdsAsync(ids);
}
}
public async Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
{
using (Profiler.TraceMethod<MongoContentRepository>())

2
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<string> CacheDependencies { get; set; }
public HashSet<object> CacheDependencies { get; set; }
}
}

12
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<IReadOnlyList<IAssetInfo>> QueryAssetsAsync(IEnumerable<Guid> assetIds)
@ -122,11 +125,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
return await assetRepository.QueryAsync(appEntity.Id, new HashSet<Guid>(assetIds));
}
private async Task<IReadOnlyList<Guid>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode)
private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(Guid filterSchemaId, FilterNode<ClrValue> filterNode)
{
return await contentRepository.QueryIdsAsync(appEntity.Id, filterSchemaId, filterNode);
}
private async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryContentsAsync(HashSet<Guid> ids)
{
return await contentRepository.QueryIdsAsync(appEntity.Id, ids);
}
private string GetScript(Func<SchemaScripts, string> script)
{
return script(schemaEntity.SchemaDef.Scripts);

14
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<IContentEntity> FindContentAsync(Guid schemaId, Guid id)
public Task<IContentEntity> 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<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, IJsonValue value)
public async Task<IReadOnlyList<IContentEntity>> 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<Guid, IContentEntity> GetContentsLoader(Guid schemaId)
private IDataLoader<Guid, IContentEntity> GetContentsLoader()
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"Schema_{schemaId}",
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>($"References",
async batch =>
{
var result = await GetReferencedContentsAsync(schemaId, new List<Guid>(batch));
var result = await GetReferencedContentsAsync(new List<Guid>(batch));
return result.ToDictionary(x => x.Id);
});

8
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);

4
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);
}

6
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<Guid>("id");
return e.FindContentAsync(schemaId, contentId);
return e.FindContentAsync(contentId);
}),
Description = $"Find an {schemaName} content by id."
});

12
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<NamedContentData>
{
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<object> PartitionResolver(ValueResolver valueResolver, string key)

86
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<IEnrichedContentEntity>
{
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<ContentInterfaceGraphType>();
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<ContentInterfaceGraphType>();
Description = $"The structure of a {schemaName} content type.";
}

84
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.";
}
}
}

30
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<Guid, IGraphType> schemaResolver;
private readonly Func<Guid, IObjectGraphType> schemaResolver;
private readonly IDictionary<ISchemaEntity, ContentGraphType> schemaTypes;
private readonly IGraphModel model;
private readonly IGraphType assetListType;
private readonly string fieldName;
public QueryGraphTypeVisitor(ISchemaEntity schema, Func<Guid, IGraphType> schemaResolver, IGraphModel model, IGraphType assetListType, string fieldName)
public QueryGraphTypeVisitor(ISchemaEntity schema,
IDictionary<ISchemaEntity, ContentGraphType> schemaTypes,
Func<Guid, IObjectGraphType> 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<ReferencesFieldProperties> 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));

61
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<Guid, IObjectGraphType> types = new Dictionary<Guid, IObjectGraphType>();
public ReferenceGraphType(string fieldName, IDictionary<ISchemaEntity, ContentGraphType> schemaTypes, IEnumerable<Guid> schemaIds, Func<Guid, IObjectGraphType> 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;
};
}
}
}

26
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<string>
result.CacheDependencies = new HashSet<object>
{
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)

4
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<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(Guid schemaId, ICollection<Guid> ids)
public virtual async Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(ICollection<Guid> 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)
{

4
src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -25,7 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, bool inDraft, ClrQuery query, bool includeDraft);
Task<IReadOnlyList<Guid>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode);
Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode);
Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids);
Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Status[] status, Guid id, bool includeDraft);

2
src/Squidex.Domain.Apps.Entities/IEntityWithCacheDependencies.cs

@ -11,6 +11,6 @@ namespace Squidex.Domain.Apps.Entities
{
public interface IEntityWithCacheDependencies
{
HashSet<string> CacheDependencies { get; }
HashSet<object> CacheDependencies { get; }
}
}

177
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>"") {
id
}
}".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId)))
.Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(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("<ID>", 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("<ID>", 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>"") {
id
}
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
.Returns(ResultList.CreateFrom<IEnrichedContentEntity>(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("<ID>", 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("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.Ignored))
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<Guid>>.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>"") {
id
data {
myUnion {
iv {
id
__typename
...MyRefSchema1Dto {
data {
ref1Field {
iv
}
}
}
}
}
}
}
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), A<IReadOnlyList<Guid>>.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("<ID>", 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<Q>.Ignored))
@ -850,10 +994,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<ID>", 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("<ID>", 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("<ID>", 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("<ID>", 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<Guid> MatchId(Guid contentId)
{
return A<IReadOnlyList<Guid>>.That.Matches(x => x.Count == 1 && x[0] == contentId);
}
private static Q MatchIdQuery(Guid contentId)
{
return A<Q>.That.Matches(x => x.Ids.Count == 1 && x.Ids[0] == contentId);
}

60
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<IContentQueryService>();
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<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
protected readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
protected readonly NamedId<Guid> schemaRefId1 = NamedId.Of(Guid.NewGuid(), "my-ref-schema1");
protected readonly NamedId<Guid> 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 = "<query-script>" })
.Publish();
.ConfigureScripts(new SchemaScripts { Query = "<query-script>" });
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<IAppProvider>();
A.CallTo(() => appProvider.GetSchemasAsync(appId.Id))
.Returns(new List<ISchemaEntity> { schema });
.Returns(new List<ISchemaEntity> { schema, schemaRef1, schemaRef2 });
var dataLoaderContext = new DataLoaderContextAccessor();

37
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<Guid> 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
};
}
}

6
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]

Loading…
Cancel
Save