Browse Source

Error handling improved for GraphQL service.

pull/95/head
Sebastian Stehle 9 years ago
parent
commit
547755a83f
  1. 9
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs
  2. 11
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs
  3. 6
      src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs
  4. 4
      src/Squidex/Config/Domain/ReadModule.cs
  5. 2
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  6. 29
      src/Squidex/Controllers/ContentApi/ContentsController.cs
  7. 13
      src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs
  8. 3
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  9. 4
      src/Squidex/app/framework/angular/http-extensions-impl.ts
  10. 30
      tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs
  11. 8
      tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj

9
src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLInvoker.cs → src/Squidex.Domain.Apps.Read/Contents/GraphQL/CachingGraphQLService.cs

@ -1,5 +1,5 @@
// ==========================================================================
// CachedGraphQLInvoker.cs
// CachingGraphQLService.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Read.Utils;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure.CQRS.Events;
using System;
using GraphQL;
using Squidex.Infrastructure.Tasks;
using Squidex.Domain.Apps.Events;
@ -24,7 +25,7 @@ using Squidex.Domain.Apps.Events;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL
{
public sealed class CachingGraphQLInvoker : CachingProviderBase, IGraphQLInvoker, IEventConsumer
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService, IEventConsumer
{
private readonly IContentRepository contentRepository;
private readonly IGraphQLUrlGenerator urlGenerator;
@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
get { return "^(schema-)|(apps-)"; }
}
public CachingGraphQLInvoker(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository, IGraphQLUrlGenerator urlGenerator)
public CachingGraphQLService(IMemoryCache cache, ISchemaRepository schemaRepository, IAssetRepository assetRepository, IContentRepository contentRepository, IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(schemaRepository, nameof(schemaRepository));
@ -70,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
return TaskHelper.Done;
}
public async Task<object> QueryAsync(IAppEntity app, GraphQLQuery query)
public async Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, GraphQLQuery query)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(query, nameof(query));

11
src/Squidex.Domain.Apps.Read/Contents/GraphQL/GraphQLModel.cs

@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
return (schemaFieldType, resolver);
}
public async Task<object> ExecuteAsync(QueryContext context, GraphQLQuery query)
public async Task<(object Data, object[] Errors)> ExecuteAsync(QueryContext context, GraphQLQuery query)
{
Guard.NotNull(context, nameof(context));
@ -186,14 +186,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
options.OperationName = query.OperationName;
}).ConfigureAwait(false);
if (result.Errors != null && result.Errors.Count > 0)
{
var errors = result.Errors.Select(x => new ValidationError(x.Message)).ToArray();
throw new ValidationException("Failed to execute GraphQL query.", errors);
}
return result;
return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());
}
public IFieldPartitioning ResolvePartition(Partitioning key)

6
src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLInvoker.cs → src/Squidex.Domain.Apps.Read/Contents/GraphQL/IGraphQLService.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IGraphQLInvoker.cs
// IGraphQLService.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,8 +11,8 @@ using Squidex.Domain.Apps.Read.Apps;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL
{
public interface IGraphQLInvoker
public interface IGraphQLService
{
Task<object> QueryAsync(IAppEntity app, GraphQLQuery query);
Task<(object Data, object[] Errors)> QueryAsync(IAppEntity app, GraphQLQuery query);
}
}

4
src/Squidex/Config/Domain/ReadModule.cs

@ -94,8 +94,8 @@ namespace Squidex.Config.Domain
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingGraphQLInvoker>()
.As<IGraphQLInvoker>()
builder.RegisterType<CachingGraphQLService>()
.As<IGraphQLService>()
.AsSelf()
.InstancePerDependency();
}

2
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -169,7 +169,7 @@ namespace Squidex.Config.Domain
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoSchemaRepository>(),
c.Resolve<CachingGraphQLInvoker>(),
c.Resolve<CachingGraphQLService>(),
c.Resolve<CachingSchemaProvider>()))
.As<IEventConsumer>()
.AsSelf()

29
src/Squidex/Controllers/ContentApi/ContentsController.cs

@ -24,6 +24,8 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
// ReSharper disable RedundantIfElseBlock
namespace Squidex.Controllers.ContentApi
{
[ApiExceptionFilter]
@ -33,32 +35,22 @@ namespace Squidex.Controllers.ContentApi
{
private readonly ISchemaProvider schemas;
private readonly IContentRepository contentRepository;
private readonly IGraphQLInvoker graphQL;
private readonly IGraphQLService graphQL;
public ContentsController(
ICommandBus commandBus,
ISchemaProvider schemas,
IContentRepository contentRepository,
IGraphQLInvoker graphQL)
IGraphQLService graphQL)
: base(commandBus)
{
this.graphQL = graphQL;
this.schemas = schemas;
this.contentRepository = contentRepository;
}
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/graphql")]
[ApiCosts(2)]
public async Task<IActionResult> GetGraphQL([FromQuery] GraphQLQuery query)
{
var result = await graphQL.QueryAsync(App, query);
return Ok(result);
}
[MustBeAppReader]
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql")]
[ApiCosts(2)]
@ -66,9 +58,16 @@ namespace Squidex.Controllers.ContentApi
{
var result = await graphQL.QueryAsync(App, query);
return Ok(result);
if (result.Errors?.Length > 0)
{
return BadRequest(new { result.Data, result.Errors });
}
else
{
return Ok(new { result.Data });
}
}
[MustBeAppReader]
[HttpGet]
[Route("content/{app}/{name}")]

13
src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs

@ -47,17 +47,24 @@ namespace Squidex.Pipeline
private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex)
{
return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 412 };
return ErrorResult(412, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnDomainException(DomainException ex)
{
return new BadRequestObjectResult(new ErrorDto { Message = ex.Message });
return ErrorResult(400, new ErrorDto { Message = ex.Message });
}
private static IActionResult OnValidationException(ValidationException ex)
{
return new BadRequestObjectResult(new ErrorDto { Message = ex.Message, Details = ex.Errors.Select(e => e.Message).ToArray() });
return ErrorResult(400, new ErrorDto { Message = ex.Message, Details = ex.Errors.Select(e => e.Message).ToArray() });
}
private static IActionResult ErrorResult(int statusCode, ErrorDto error)
{
error.StatusCode = statusCode;
return new ObjectResult(error) { StatusCode = 412 };
}
public override void OnActionExecuting(ActionExecutingContext context)

3
src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts

@ -6,6 +6,7 @@
*/
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@ -56,7 +57,7 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
private request(params: any) {
return this.appNameOnce()
.switchMap(app => this.graphQlService.query(app, params))
.switchMap(app => this.graphQlService.query(app, params).catch(response => Observable.of(response.error)))
.toPromise();
}
}

4
src/Squidex/app/framework/angular/http-extensions-impl.ts

@ -96,11 +96,13 @@ export function pretifyError(message: string): Observable<any> {
let result = new ErrorDto(500, message);
if (!(response.error instanceof Error)) {
const errorDto = response.error;
try {
if (response.status === 412) {
result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.');
} else if (response.status !== 500) {
result = new ErrorDto(response.status, response.error.message, response.error.details);
result = new ErrorDto(response.status, errorDto.message, errorDto.details);
}
} catch (e) {
/* Ignore */

30
tests/Squidex.Domain.Apps.Read.Tests/Contents/GraphQLTests.cs

@ -29,6 +29,8 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Infrastructure;
// ReSharper disable SimilarAnonymousTypeNearby
namespace Squidex.Domain.Apps.Read.Contents
{
public class GraphQLTests
@ -62,7 +64,7 @@ namespace Squidex.Domain.Apps.Read.Contents
private readonly Mock<IAssetRepository> assetRepository = new Mock<IAssetRepository>();
private readonly IAppEntity app;
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IGraphQLInvoker sut;
private readonly IGraphQLService sut;
public GraphQLTests()
{
@ -82,7 +84,7 @@ namespace Squidex.Domain.Apps.Read.Contents
schemaRepository.Setup(x => x.QueryAllAsync(appId)).Returns(Task.FromResult<IReadOnlyList<ISchemaEntity>>(schemas));
sut = new CachingGraphQLInvoker(cache, schemaRepository.Object, assetRepository.Object, contentRepository.Object, new FakeUrlGenerator());
sut = new CachingGraphQLService(cache, schemaRepository.Object, assetRepository.Object, contentRepository.Object, new FakeUrlGenerator());
}
[Fact]
@ -117,7 +119,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(assets))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -147,7 +149,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
assetRepository.VerifyAll();
}
@ -183,7 +185,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult(assetEntity))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -210,7 +212,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
assetRepository.VerifyAll();
}
@ -259,7 +261,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(contents))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -315,7 +317,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll();
}
@ -363,7 +365,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult(contentEntity))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -416,7 +418,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll();
}
@ -454,7 +456,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(refContents))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -480,7 +482,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll();
}
@ -518,7 +520,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(refAssets))
.Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new
{
@ -544,7 +546,7 @@ namespace Squidex.Domain.Apps.Read.Contents
}
};
AssertJson(expected, result);
AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll();
}

8
tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj

@ -5,6 +5,11 @@
<PackageTargetFallback>$(PackageTargetFallback);dnxcore50</PackageTargetFallback>
<RootNamespace>Squidex.Domain.Apps.Read</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Remove="MongoDb\**" />
<EmbeddedResource Remove="MongoDb\**" />
<None Remove="MongoDb\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core\Squidex.Domain.Apps.Core.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
@ -26,7 +31,4 @@
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup>
<Folder Include="MongoDb\Contents\" />
</ItemGroup>
</Project>
Loading…
Cancel
Save