Browse Source

Fix content ordering when querying by ids.

pull/305/head
Sebastian 8 years ago
parent
commit
9585aa4a4f
  1. 27
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  2. 2
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  4. 7
      src/Squidex.Infrastructure/CollectionExtensions.cs
  5. 4
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  6. 72
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs
  7. 2
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

27
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity)); throw new DomainObjectNotFoundException(id.ToString(), typeof(ISchemaEntity));
} }
return TransformContent(context, schema, true, content); return Transform(context, schema, true, content);
} }
} }
@ -100,11 +100,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, parsedQuery); var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, parsedQuery);
return TransformContents(context, schema, true, contents); return Transform(context, schema, true, contents);
} }
} }
public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, HashSet<Guid> ids) public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, IList<Guid> ids)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));
@ -115,25 +115,32 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var parsedStatus = ParseStatus(context); var parsedStatus = ParseStatus(context);
var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, ids); var contents = await contentRepository.QueryAsync(context.App, schema, parsedStatus, new HashSet<Guid>(ids));
return TransformContents(context, schema, false, contents); return Sort(Transform(context, schema, false, contents), ids);
} }
} }
private IContentEntity TransformContent(QueryContext context, ISchemaEntity schema, bool checkType, IContentEntity content) private IContentEntity Transform(QueryContext context, ISchemaEntity schema, bool checkType, IContentEntity content)
{ {
return TransformContents(context, schema, checkType, Enumerable.Repeat(content, 1)).FirstOrDefault(); return Transform(context, schema, checkType, Enumerable.Repeat(content, 1)).FirstOrDefault();
} }
private IResultList<IContentEntity> TransformContents(QueryContext context, ISchemaEntity schema, bool checkType, IResultList<IContentEntity> contents) private IResultList<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, bool checkType, IResultList<IContentEntity> contents)
{ {
var transformed = TransformContents(context, schema, checkType, (IEnumerable<IContentEntity>)contents); var transformed = Transform(context, schema, checkType, (IEnumerable<IContentEntity>)contents);
return ResultList.Create(transformed, contents.Total); return ResultList.Create(transformed, contents.Total);
} }
private IEnumerable<IContentEntity> TransformContents(QueryContext context, ISchemaEntity schema, bool checkType, IEnumerable<IContentEntity> contents) private IResultList<IContentEntity> Sort(IResultList<IContentEntity> contents, IList<Guid> ids)
{
var sorted = ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
return ResultList.Create(sorted, contents.Total);
}
private IEnumerable<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, bool checkType, IEnumerable<IContentEntity> contents)
{ {
using (Profiler.TraceMethod<ContentQueryService>()) using (Profiler.TraceMethod<ContentQueryService>())
{ {

2
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -14,7 +14,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentQueryService public interface IContentQueryService
{ {
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, HashSet<Guid> ids); Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, IList<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string query); Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string query);

2
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -118,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
Guard.NotNull(ids, nameof(ids)); Guard.NotNull(ids, nameof(ids));
var notLoadedContents = new HashSet<Guid>(ids.Where(id => !cachedContents.ContainsKey(id))); var notLoadedContents = ids.Where(id => !cachedContents.ContainsKey(id)).ToList();
if (notLoadedContents.Count > 0) if (notLoadedContents.Count > 0)
{ {

7
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -14,6 +14,13 @@ namespace Squidex.Infrastructure
{ {
public static class CollectionExtensions public static class CollectionExtensions
{ {
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{
var random = new Random();
return enumerable.OrderBy(x => random.Next()).ToList();
}
public static ImmutableDictionary<TKey, TValue> SetItem<TKey, TValue>(this ImmutableDictionary<TKey, TValue> dictionary, TKey key, Func<TValue, TValue> updater) public static ImmutableDictionary<TKey, TValue> SetItem<TKey, TValue>(this ImmutableDictionary<TKey, TValue> dictionary, TKey key, Func<TValue, TValue> updater)
{ {
if (dictionary.TryGetValue(key, out var value)) if (dictionary.TryGetValue(key, out var value))

4
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -97,11 +97,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
[ApiCosts(2)] [ApiCosts(2)]
public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null) public async Task<IActionResult> GetContents(string app, string name, [FromQuery] bool archived = false, [FromQuery] string ids = null)
{ {
HashSet<Guid> idsList = null; List<Guid> idsList = null;
if (!string.IsNullOrWhiteSpace(ids)) if (!string.IsNullOrWhiteSpace(ids))
{ {
idsList = new HashSet<Guid>(); idsList = new List<Guid>();
foreach (var id in ids.Split(',')) foreach (var id in ids.Split(','))
{ {

72
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs

@ -35,12 +35,10 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IContentVersionLoader contentVersionLoader = A.Fake<IContentVersionLoader>(); private readonly IContentVersionLoader contentVersionLoader = A.Fake<IContentVersionLoader>();
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>(); private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IContentEntity content = A.Fake<IContentEntity>();
private readonly IAppEntity app = A.Fake<IAppEntity>(); private readonly IAppEntity app = A.Fake<IAppEntity>();
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
private readonly Guid schemaId = Guid.NewGuid(); private readonly Guid schemaId = Guid.NewGuid();
private readonly Guid contentId = Guid.NewGuid();
private readonly string appName = "my-app"; private readonly string appName = "my-app";
private readonly NamedContentData contentData = new NamedContentData(); private readonly NamedContentData contentData = new NamedContentData();
private readonly NamedContentData contentTransformed = new NamedContentData(); private readonly NamedContentData contentTransformed = new NamedContentData();
@ -58,11 +56,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => app.Name).Returns(appName); A.CallTo(() => app.Name).Returns(appName);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English); A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.English);
A.CallTo(() => content.Id).Returns(contentId);
A.CallTo(() => content.Data).Returns(contentData);
A.CallTo(() => content.DataDraft).Returns(contentData);
A.CallTo(() => content.Status).Returns(Status.Published);
A.CallTo(() => schema.SchemaDef).Returns(new Schema("my-schema")); A.CallTo(() => schema.SchemaDef).Returns(new Schema("my-schema"));
context = QueryContext.Create(app, user); context = QueryContext.Create(app, user);
@ -120,19 +113,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
[MemberData(nameof(SingleRequestData))] [MemberData(nameof(SingleRequestData))]
public async Task Should_return_content_from_repository_and_transform(bool isFrontend, params Status[] status) public async Task Should_return_content_from_repository_and_transform(bool isFrontend, params Status[] status)
{ {
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupClaims(isFrontend); SetupClaims(isFrontend);
SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), contentId)) A.CallTo(() => contentRepository.FindContentAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), contentId))
.Returns(content); .Returns(content);
A.CallTo(() => schema.ScriptQuery)
.Returns("<script-query>");
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, contentData)), "<query-script>"))
.Returns(contentTransformed);
var result = await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId); var result = await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId);
Assert.Equal(contentTransformed, result.Data); Assert.Equal(contentTransformed, result.Data);
@ -142,17 +133,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_return_versioned_content_from_repository_and_transform() public async Task Should_return_versioned_content_from_repository_and_transform()
{ {
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10)) A.CallTo(() => contentVersionLoader.LoadAsync(contentId, 10))
.Returns(content); .Returns(content);
A.CallTo(() => schema.ScriptQuery)
.Returns("<script-query>");
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, contentData)), "<query-script>"))
.Returns(contentTransformed);
var result = await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId, 10); var result = await sut.FindContentAsync(context.WithSchemaId(schemaId), contentId, 10);
Assert.Equal(contentTransformed, result.Data); Assert.Equal(contentTransformed, result.Data);
@ -162,6 +152,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
[Fact] [Fact]
public async Task Should_throw_if_content_to_find_does_not_exist() public async Task Should_throw_if_content_to_find_does_not_exist()
{ {
var contentId = Guid.NewGuid();
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
@ -183,8 +175,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
[MemberData(nameof(ManyRequestData))] [MemberData(nameof(ManyRequestData))]
public async Task Should_query_contents_by_query_from_repository_and_transform(int count, int total, bool isFrontend, bool archive, params Status[] status) public async Task Should_query_contents_by_query_from_repository_and_transform(int count, int total, bool isFrontend, bool archive, params Status[] status)
{ {
var contentId = Guid.NewGuid();
var content = CreateContent(contentId);
SetupClaims(isFrontend); SetupClaims(isFrontend);
SetupFakeWithScripting(); SetupScripting(contentId);
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
@ -235,22 +230,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
[MemberData(nameof(ManyIdRequestData))] [MemberData(nameof(ManyIdRequestData))]
public async Task Should_query_contents_by_id_from_repository_and_transform(int count, int total, bool isFrontend, bool archive, params Status[] status) public async Task Should_query_contents_by_id_from_repository_and_transform(int count, int total, bool isFrontend, bool archive, params Status[] status)
{ {
var ids = new HashSet<Guid>(Enumerable.Range(0, count).Select(x => Guid.NewGuid())); var ids = Enumerable.Range(0, count).Select(x => Guid.NewGuid()).ToList();
SetupClaims(isFrontend); SetupClaims(isFrontend);
SetupFakeWithScripting(); SetupScripting(ids.ToArray());
A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false))
.Returns(schema); .Returns(schema);
A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), ids)) A.CallTo(() => contentRepository.QueryAsync(app, schema, A<Status[]>.That.IsSameSequenceAs(status), A<HashSet<Guid>>.Ignored))
.Returns(ResultList.Create(Enumerable.Repeat(content, count), total)); .Returns(ResultList.Create(ids.Select(x => CreateContent(x)).Shuffle(), total));
var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), ids); var result = await sut.QueryAsync(context.WithSchemaId(schemaId).WithArchived(archive), ids);
Assert.Equal(contentData, result[0].Data); Assert.Equal(ids, result.Select(x => x.Id).ToList());
Assert.Equal(content.Id, result[0].Id);
Assert.Equal(total, result.Total); Assert.Equal(total, result.Total);
if (!isFrontend) if (!isFrontend)
@ -273,13 +266,30 @@ namespace Squidex.Domain.Apps.Entities.Contents
} }
} }
private void SetupFakeWithScripting() private void SetupScripting(params Guid[] contentId)
{ {
var script = "<script-query>";
A.CallTo(() => schema.ScriptQuery) A.CallTo(() => schema.ScriptQuery)
.Returns("<script-query>"); .Returns(script);
foreach (var id in contentId)
{
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == id && x.Data == contentData), script))
.Returns(contentTransformed);
}
}
private IContentEntity CreateContent(Guid id, Status status = Status.Published)
{
var content = A.Fake<IContentEntity>();
A.CallTo(() => content.Id).Returns(id);
A.CallTo(() => content.Data).Returns(contentData);
A.CallTo(() => content.DataDraft).Returns(contentData);
A.CallTo(() => content.Status).Returns(status);
A.CallTo(() => scriptEngine.Transform(A<ScriptContext>.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, contentData)), "<query-script>")) return content;
.Returns(contentTransformed);
} }
} }
} }

2
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -635,7 +635,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any)) A.CallTo(() => contentQuery.FindContentAsync(ContextMatch(), contentId, EtagVersion.Any))
.Returns(content); .Returns(content);
A.CallTo(() => contentQuery.QueryAsync(ContextMatch(), A<HashSet<Guid>>.That.Matches(x => x.Contains(contentRefId)))) A.CallTo(() => contentQuery.QueryAsync(ContextMatch(), A<IList<Guid>>.That.IsSameSequenceAs(new[] { contentRefId })))
.Returns(ResultList.Create(refContents, 0)); .Returns(ResultList.Create(refContents, 0));
var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(context, new GraphQLQuery { Query = query });

Loading…
Cancel
Save