diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 7fd0c7f85..2fd9e298b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/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; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs index 8e859e6a8..3dd0d28a5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs @@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetLoader { - Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); + Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs index f6aec087f..b84854b0d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ b/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 GetAsync(DomainId appId, DomainId id, long version) + public async Task GetAsync(DomainId appId, DomainId id, long version) { using (Profiler.TraceMethod()) { @@ -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; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 20fd1f04c..50fcc1c66 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/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; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index 498e1e33e..d65f0e4e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/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." }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs index d8f960d04..6a9654efe 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs +++ b/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).", 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(c => + public static IFieldResolver Resolver(DomainId schemaId) { - var contentId = c.GetArgument("id"); + var schemaIdValue = schemaId.ToString(); - return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId); - }); + return new FuncFieldResolver(c => + { + var contentId = c.GetArgument("id"); + + var version = c.GetArgument("version"); + + if (version >= 0) + { + return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(schemaIdValue, contentId, version.Value); + } + else + { + return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId); + } + }); + } } public static class QueryOrReferencing diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs index ddb4d22bb..52215702f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs @@ -12,6 +12,6 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentLoader { - Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); + Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 5bc49bff4..89a065fa4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Task> QueryAsync(Context context, string schemaIdOrName, Q query); - Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any); + Task FindAsync(Context context, string schemaIdOrName, DomainId id, long version = EtagVersion.Any); Task GetSchemaOrThrowAsync(Context context, string schemaIdOrName); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs index d9370d53d..0bdbb3f8a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ b/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 GetAsync(DomainId appId, DomainId id, long version) + public async Task GetAsync(DomainId appId, DomainId id, long version) { using (Profiler.TraceMethod()) { @@ -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; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 28828bf0c..92d6c6ec1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/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 FindAsync(Context context, string schemaIdOrName, DomainId id, long version = -1) + public async Task 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 FindByVersionAsync(Context context, DomainId id, long version) + private Task FindByVersionAsync(Context context, DomainId id, long version) { return contentVersionLoader.GetAsync(context.App.Id, id, version); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 35c392765..4a7a793fa 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/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 FindContentAsync(string schemaIdOrName, DomainId id, long version) + { + return contentQuery.FindAsync(context, schemaIdOrName, id, version); + } + public virtual async Task FindAssetAsync(DomainId id) { var asset = cachedAssets.GetOrDefault(id); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 0ed3600a9..75f200616 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/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); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs index 390d32ff2..d9021e398 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ b/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(null!)); - await Assert.ThrowsAsync(() => 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(content)); - await Assert.ThrowsAsync(() => 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(content)); - await Assert.ThrowsAsync(() => sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10)); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs index 7a0943606..9f1adc8b4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs +++ b/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: """", version: 3) { + + } + }".Replace("", TestContent.AllFields).Replace("", 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() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs index 874c441ad..9464e0903 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ b/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(null!)); - await Assert.ThrowsAsync(() => 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(content)); - await Assert.ThrowsAsync(() => 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(content)); - await Assert.ThrowsAsync(() => 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 }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 1da50dc68..fc3bf9b3a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/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._)) .Returns(null); - await Assert.ThrowsAsync(async () => await sut.FindAsync(ctx, schemaId.Name, contentId)); + Assert.Null(await sut.FindAsync(ctx, schemaId.Name, contentId)); } [Theory]