Browse Source

Feature/version-in-graphql (#622)

* Get content by version in graphql.

* Return null instead of exception.
pull/624/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
e8c7bc7f38
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs
  3. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs
  4. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  5. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  6. 29
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  9. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs
  10. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  11. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  12. 10
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  13. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs
  14. 33
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  15. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs
  16. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

5
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -73,6 +73,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
@event.Payload.AssetId, @event.Payload.AssetId,
@event.Headers.EventStreamNumber()); @event.Headers.EventStreamNumber());
if (asset == null)
{
throw new DomainObjectNotFoundException(@event.Payload.AssetId.ToString());
}
SimpleMapper.Map(asset, result); SimpleMapper.Map(asset, result);
result.AssetType = asset.Type; result.AssetType = asset.Type;

2
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs

@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetLoader public interface IAssetLoader
{ {
Task<IAssetEntity> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any);
} }
} }

4
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public async Task<IAssetEntity> GetAsync(DomainId appId, DomainId id, long version) public async Task<IAssetEntity?> GetAsync(DomainId appId, DomainId id, long version)
{ {
using (Profiler.TraceMethod<AssetLoader>()) using (Profiler.TraceMethod<AssetLoader>())
{ {
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
if (asset == null || asset.Version <= EtagVersion.Empty || (version > EtagVersion.Any && asset.Version != version)) if (asset == null || asset.Version <= EtagVersion.Empty || (version > EtagVersion.Any && asset.Version != version))
{ {
throw new DomainObjectNotFoundException(id.ToString()); return null;
} }
return asset; return asset;

10
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -77,6 +77,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
@event.Payload.ContentId, @event.Payload.ContentId,
@event.Headers.EventStreamNumber()); @event.Headers.EventStreamNumber());
if (content == null)
{
throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString());
}
SimpleMapper.Map(content, result); SimpleMapper.Map(content, result);
switch (@event.Payload) switch (@event.Payload)
@ -116,6 +121,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
content.Id, content.Id,
content.Version - 1); content.Version - 1);
if (previousContent == null)
{
throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString());
}
result.DataOld = previousContent.Data; result.DataOld = previousContent.Data;
break; break;
} }

18
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -29,8 +29,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var contentType = model.GetContentType(schema.Id); var contentType = model.GetContentType(schema.Id);
AddContentFind(schemaType, schemaName, contentType); AddContentFind(
AddContentQueries(schemaId, schemaType, schemaName, contentType, pageSizeContents); schemaId,
schemaType,
schemaName,
contentType);
AddContentQueries(
schemaId,
schemaType,
schemaName,
contentType,
pageSizeContents);
} }
Description = "The app queries."; Description = "The app queries.";
@ -48,14 +58,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}); });
} }
private void AddContentFind(string schemaType, string schemaName, IGraphType contentType) private void AddContentFind(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = $"find{schemaType}Content", Name = $"find{schemaType}Content",
Arguments = ContentActions.Find.Arguments, Arguments = ContentActions.Find.Arguments,
ResolvedType = contentType, ResolvedType = contentType,
Resolver = ContentActions.Find.Resolver, Resolver = ContentActions.Find.Resolver(schemaId),
Description = $"Find an {schemaName} content by id." Description = $"Find an {schemaName} content by id."
}); });
} }

29
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs

@ -71,15 +71,36 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The id of the content (usually GUID).", Description = "The id of the content (usually GUID).",
DefaultValue = null, DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None)
{
Name = "version",
Description = "The optional version of the content to retrieve an older instance (not cached).",
DefaultValue = null,
ResolvedType = AllTypes.Int
} }
}; };
public static readonly IFieldResolver Resolver = new FuncFieldResolver<object?>(c => public static IFieldResolver Resolver(DomainId schemaId)
{ {
var contentId = c.GetArgument<DomainId>("id"); var schemaIdValue = schemaId.ToString();
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId); return new FuncFieldResolver<object?>(c =>
}); {
var contentId = c.GetArgument<DomainId>("id");
var version = c.GetArgument<int?>("version");
if (version >= 0)
{
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(schemaIdValue, contentId, version.Value);
}
else
{
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId);
}
});
}
} }
public static class QueryOrReferencing public static class QueryOrReferencing

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs

@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentLoader public interface IContentLoader
{ {
Task<IContentEntity> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any);
} }
} }

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

@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query); Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query);
Task<IEnrichedContentEntity> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any); Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any);
Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName); Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName);

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public async Task<IContentEntity> GetAsync(DomainId appId, DomainId id, long version) public async Task<IContentEntity?> GetAsync(DomainId appId, DomainId id, long version)
{ {
using (Profiler.TraceMethod<ContentLoader>()) using (Profiler.TraceMethod<ContentLoader>())
{ {
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (content == null || content.Version <= EtagVersion.Empty || (version > EtagVersion.Any && content.Version != version)) if (content == null || content.Version <= EtagVersion.Empty || (version > EtagVersion.Any && content.Version != version))
{ {
throw new DomainObjectNotFoundException(id.ToString()); return null;
} }
return content; return content;

6
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.queryParser = queryParser; this.queryParser = queryParser;
} }
public async Task<IEnrichedContentEntity> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) public async Task<IEnrichedContentEntity?> FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (content == null || content.SchemaId.Id != schema.Id) if (content == null || content.SchemaId.Id != schema.Id)
{ {
throw new DomainObjectNotFoundException(id.ToString()); return null;
} }
return await TransformAsync(context, content); return await TransformAsync(context, content);
@ -220,7 +220,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return contentRepository.FindContentAsync(context.App, schema, id, context.Scope()); return contentRepository.FindContentAsync(context.App, schema, id, context.Scope());
} }
private Task<IContentEntity> FindByVersionAsync(Context context, DomainId id, long version) private Task<IContentEntity?> FindByVersionAsync(Context context, DomainId id, long version)
{ {
return contentVersionLoader.GetAsync(context.App.Id, id, version); return contentVersionLoader.GetAsync(context.App.Id, id, version);
} }

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -40,6 +40,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
this.context = context; this.context = context;
} }
public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version)
{
return contentQuery.FindAsync(context, schemaIdOrName, id, version);
}
public virtual async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id) public virtual async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id)
{ {
var asset = cachedAssets.GetOrDefault(id); var asset = cachedAssets.GetOrDefault(id);

10
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -280,6 +280,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var content = await contentQuery.FindAsync(Context, name, id); var content = await contentQuery.FindAsync(Context, name, id);
if (content == null)
{
return NotFound();
}
var response = ContentDto.FromContent(content, Resources); var response = ContentDto.FromContent(content, Resources);
return Ok(response); return Ok(response);
@ -396,6 +401,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
var content = await contentQuery.FindAsync(Context, name, id, version); var content = await contentQuery.FindAsync(Context, name, id, version);
if (content == null)
{
return NotFound();
}
var response = ContentDto.FromContent(content, Resources); var response = ContentDto.FromContent(content, Resources);
return Ok(response.Data); return Ok(response.Data);

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs

@ -34,34 +34,34 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_no_state_returned() public async Task Should_return_null_if_no_state_returned()
{ {
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IAssetEntity>(null!)); .Returns(J.Of<IAssetEntity>(null!));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_state_empty() public async Task Should_return_null_if_state_empty()
{ {
var content = new AssetEntity { Version = EtagVersion.Empty }; var content = new AssetEntity { Version = EtagVersion.Empty };
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IAssetEntity>(content)); .Returns(J.Of<IAssetEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_state_has_other_version() public async Task Should_return_null_if_state_has_other_version()
{ {
var content = new AssetEntity { Version = 5 }; var content = new AssetEntity { Version = 5 };
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IAssetEntity>(content)); .Returns(J.Of<IAssetEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]

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

@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
[Fact] [Fact]
public async Task Should_return_null_single_asset() public async Task Should_return_null_single_asset_when_not_found()
{ {
var assetId = DomainId.NewGuid(); var assetId = DomainId.NewGuid();
@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
[Fact] [Fact]
public async Task Should_return_null_single_content() public async Task Should_return_null_single_content_when_not_found()
{ {
var contentId = DomainId.NewGuid(); var contentId = DomainId.NewGuid();
@ -388,6 +388,35 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
AssertResult(expected, result); AssertResult(expected, result);
} }
[Fact]
public async Task Should_return_single_content_when_finding_content_with_version()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(appId, schemaId, contentId, DomainId.Empty, DomainId.Empty);
var query = @"
query {
findMySchemaContent(id: ""<ID>"", version: 3) {
<FIELDS>
}
}".Replace("<FIELDS>", TestContent.AllFields).Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.FindAsync(MatchsContentContext(), schemaId.Id.ToString(), contentId, 3))
.Returns(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
findMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, result);
}
[Fact] [Fact]
public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query() public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query()
{ {

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs

@ -34,38 +34,38 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_no_state_returned() public async Task Should_return_null_if_no_state_returned()
{ {
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(null!)); .Returns(J.Of<IContentEntity>(null!));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_state_empty() public async Task Should_return_null_if_state_empty()
{ {
var content = new ContentEntity { Version = EtagVersion.Empty }; var content = new ContentEntity { Version = EtagVersion.Empty };
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content)); .Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]
public async Task Should_throw_exception_if_state_has_other_version() public async Task Should_return_null_if_state_has_other_version()
{ {
var content = new ContentEntity { Version = 5 }; var content = new ContentEntity { Version = 5 };
A.CallTo(() => grain.GetStateAsync(10)) A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content)); .Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10)); Assert.Null(await sut.GetAsync(appId, id, 10));
} }
[Fact] [Fact]
public async Task Should_not_throw_exception_if_state_has_other_version_than_any() public async Task Should_not_return_null_if_state_has_other_version_than_any()
{ {
var content = new ContentEntity { Version = 5 }; var content = new ContentEntity { Version = 5 };

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs

@ -119,14 +119,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
} }
[Fact] [Fact]
public async Task FindContentAsync_should_throw_404_if_not_found() public async Task FindContentAsync_should_return_null_if_not_found()
{ {
var ctx = CreateContext(isFrontend: false, allowSchema: true); var ctx = CreateContext(isFrontend: false, allowSchema: true);
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._)) A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
.Returns<IContentEntity?>(null); .Returns<IContentEntity?>(null);
await Assert.ThrowsAsync<DomainObjectNotFoundException>(async () => await sut.FindAsync(ctx, schemaId.Name, contentId)); Assert.Null(await sut.FindAsync(ctx, schemaId.Name, contentId));
} }
[Theory] [Theory]

Loading…
Cancel
Save