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. 23
      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.Headers.EventStreamNumber());
if (asset == null)
{
throw new DomainObjectNotFoundException(@event.Payload.AssetId.ToString());
}
SimpleMapper.Map(asset, result);
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
{
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;
}
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>())
{
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
if (asset == null || asset.Version <= EtagVersion.Empty || (version > EtagVersion.Any && asset.Version != version))
{
throw new DomainObjectNotFoundException(id.ToString());
return null;
}
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.Headers.EventStreamNumber());
if (content == null)
{
throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString());
}
SimpleMapper.Map(content, result);
switch (@event.Payload)
@ -116,6 +121,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
content.Id,
content.Version - 1);
if (previousContent == null)
{
throw new DomainObjectNotFoundException(@event.Payload.ContentId.ToString());
}
result.DataOld = previousContent.Data;
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);
AddContentFind(schemaType, schemaName, contentType);
AddContentQueries(schemaId, schemaType, schemaName, contentType, pageSizeContents);
AddContentFind(
schemaId,
schemaType,
schemaName,
contentType);
AddContentQueries(
schemaId,
schemaType,
schemaName,
contentType,
pageSizeContents);
}
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
{
Name = $"find{schemaType}Content",
Arguments = ContentActions.Find.Arguments,
ResolvedType = contentType,
Resolver = ContentActions.Find.Resolver,
Resolver = ContentActions.Find.Resolver(schemaId),
Description = $"Find an {schemaName} content by id."
});
}

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

@ -71,16 +71,37 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The id of the content (usually GUID).",
DefaultValue = null,
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 schemaIdValue = schemaId.ToString();
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
{

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

@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
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<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);

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;
}
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>())
{
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (content == null || content.Version <= EtagVersion.Empty || (version > EtagVersion.Any && content.Version != version))
{
throw new DomainObjectNotFoundException(id.ToString());
return null;
}
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;
}
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));
@ -74,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (content == null || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString());
return null;
}
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());
}
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);
}

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;
}
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)
{
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);
if (content == null)
{
return NotFound();
}
var response = ContentDto.FromContent(content, Resources);
return Ok(response);
@ -396,6 +401,11 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var content = await contentQuery.FindAsync(Context, name, id, version);
if (content == null)
{
return NotFound();
}
var response = ContentDto.FromContent(content, Resources);
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]
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))
.Returns(J.Of<IAssetEntity>(null!));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IAssetEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IAssetEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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]
public async Task Should_return_null_single_asset()
public async Task Should_return_null_single_asset_when_not_found()
{
var assetId = DomainId.NewGuid();
@ -332,7 +332,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
[Fact]
public async Task Should_return_null_single_content()
public async Task Should_return_null_single_content_when_not_found()
{
var contentId = DomainId.NewGuid();
@ -388,6 +388,35 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
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]
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]
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))
.Returns(J.Of<IContentEntity>(null!));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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 };
A.CallTo(() => grain.GetStateAsync(10))
.Returns(J.Of<IContentEntity>(content));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetAsync(appId, id, 10));
Assert.Null(await sut.GetAsync(appId, id, 10));
}
[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 };

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

@ -119,14 +119,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
[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);
A.CallTo(() => contentRepository.FindContentAsync(ctx.App, schema, contentId, A<SearchScope>._))
.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]

Loading…
Cancel
Save