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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Read.Utils;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using System; using System;
using GraphQL;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
@ -24,7 +25,7 @@ using Squidex.Domain.Apps.Events;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL 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 IContentRepository contentRepository;
private readonly IGraphQLUrlGenerator urlGenerator; private readonly IGraphQLUrlGenerator urlGenerator;
@ -41,7 +42,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
get { return "^(schema-)|(apps-)"; } 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) : base(cache)
{ {
Guard.NotNull(schemaRepository, nameof(schemaRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository));
@ -70,7 +71,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
return TaskHelper.Done; 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(app, nameof(app));
Guard.NotNull(query, nameof(query)); 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); 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)); Guard.NotNull(context, nameof(context));
@ -186,14 +186,7 @@ namespace Squidex.Domain.Apps.Read.Contents.GraphQL
options.OperationName = query.OperationName; options.OperationName = query.OperationName;
}).ConfigureAwait(false); }).ConfigureAwait(false);
if (result.Errors != null && result.Errors.Count > 0) return (result.Data, result.Errors?.Select(x => (object)new { x.Message, x.Locations }).ToArray());
{
var errors = result.Errors.Select(x => new ValidationError(x.Message)).ToArray();
throw new ValidationException("Failed to execute GraphQL query.", errors);
}
return result;
} }
public IFieldPartitioning ResolvePartition(Partitioning key) 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -11,8 +11,8 @@ using Squidex.Domain.Apps.Read.Apps;
namespace Squidex.Domain.Apps.Read.Contents.GraphQL 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() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<CachingGraphQLInvoker>() builder.RegisterType<CachingGraphQLService>()
.As<IGraphQLInvoker>() .As<IGraphQLService>()
.AsSelf() .AsSelf()
.InstancePerDependency(); .InstancePerDependency();
} }

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

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

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

@ -24,6 +24,8 @@ using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline; using Squidex.Pipeline;
// ReSharper disable RedundantIfElseBlock
namespace Squidex.Controllers.ContentApi namespace Squidex.Controllers.ContentApi
{ {
[ApiExceptionFilter] [ApiExceptionFilter]
@ -33,32 +35,22 @@ namespace Squidex.Controllers.ContentApi
{ {
private readonly ISchemaProvider schemas; private readonly ISchemaProvider schemas;
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
private readonly IGraphQLInvoker graphQL; private readonly IGraphQLService graphQL;
public ContentsController( public ContentsController(
ICommandBus commandBus, ICommandBus commandBus,
ISchemaProvider schemas, ISchemaProvider schemas,
IContentRepository contentRepository, IContentRepository contentRepository,
IGraphQLInvoker graphQL) IGraphQLService graphQL)
: base(commandBus) : base(commandBus)
{ {
this.graphQL = graphQL; this.graphQL = graphQL;
this.schemas = schemas; this.schemas = schemas;
this.contentRepository = contentRepository; 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] [MustBeAppReader]
[HttpGet]
[HttpPost] [HttpPost]
[Route("content/{app}/graphql")] [Route("content/{app}/graphql")]
[ApiCosts(2)] [ApiCosts(2)]
@ -66,9 +58,16 @@ namespace Squidex.Controllers.ContentApi
{ {
var result = await graphQL.QueryAsync(App, query); 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] [MustBeAppReader]
[HttpGet] [HttpGet]
[Route("content/{app}/{name}")] [Route("content/{app}/{name}")]

13
src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs

@ -47,17 +47,24 @@ namespace Squidex.Pipeline
private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) 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) 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) 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) 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 { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Observable } from 'rxjs';
import * as React from 'react'; import * as React from 'react';
import * as ReactDOM from 'react-dom'; import * as ReactDOM from 'react-dom';
@ -56,7 +57,7 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
private request(params: any) { private request(params: any) {
return this.appNameOnce() return this.appNameOnce()
.switchMap(app => this.graphQlService.query(app, params)) .switchMap(app => this.graphQlService.query(app, params).catch(response => Observable.of(response.error)))
.toPromise(); .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); let result = new ErrorDto(500, message);
if (!(response.error instanceof Error)) { if (!(response.error instanceof Error)) {
const errorDto = response.error;
try { try {
if (response.status === 412) { if (response.status === 412) {
result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.'); result = new ErrorDto(response.status, 'Failed to make the update. Another user has made a change. Please reload.');
} else if (response.status !== 500) { } 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) { } catch (e) {
/* Ignore */ /* 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.Domain.Apps.Read.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
// ReSharper disable SimilarAnonymousTypeNearby
namespace Squidex.Domain.Apps.Read.Contents namespace Squidex.Domain.Apps.Read.Contents
{ {
public class GraphQLTests public class GraphQLTests
@ -62,7 +64,7 @@ namespace Squidex.Domain.Apps.Read.Contents
private readonly Mock<IAssetRepository> assetRepository = new Mock<IAssetRepository>(); private readonly Mock<IAssetRepository> assetRepository = new Mock<IAssetRepository>();
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IGraphQLInvoker sut; private readonly IGraphQLService sut;
public GraphQLTests() public GraphQLTests()
{ {
@ -82,7 +84,7 @@ namespace Squidex.Domain.Apps.Read.Contents
schemaRepository.Setup(x => x.QueryAllAsync(appId)).Returns(Task.FromResult<IReadOnlyList<ISchemaEntity>>(schemas)); 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] [Fact]
@ -117,7 +119,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(assets)) .Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(assets))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -147,7 +149,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
assetRepository.VerifyAll(); assetRepository.VerifyAll();
} }
@ -183,7 +185,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult(assetEntity)) .Returns(Task.FromResult(assetEntity))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -210,7 +212,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
assetRepository.VerifyAll(); assetRepository.VerifyAll();
} }
@ -259,7 +261,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(contents)) .Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(contents))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -315,7 +317,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll(); contentRepository.VerifyAll();
} }
@ -363,7 +365,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult(contentEntity)) .Returns(Task.FromResult(contentEntity))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -416,7 +418,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll(); contentRepository.VerifyAll();
} }
@ -454,7 +456,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(refContents)) .Returns(Task.FromResult<IReadOnlyList<IContentEntity>>(refContents))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -480,7 +482,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll(); contentRepository.VerifyAll();
} }
@ -518,7 +520,7 @@ namespace Squidex.Domain.Apps.Read.Contents
.Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(refAssets)) .Returns(Task.FromResult<IReadOnlyList<IAssetEntity>>(refAssets))
.Verifiable(); .Verifiable();
dynamic result = await sut.QueryAsync(app, new GraphQLQuery { Query = query }); var result = await sut.QueryAsync(app, new GraphQLQuery { Query = query });
var expected = new var expected = new
{ {
@ -544,7 +546,7 @@ namespace Squidex.Domain.Apps.Read.Contents
} }
}; };
AssertJson(expected, result); AssertJson(expected, new { data = result.Data });
contentRepository.VerifyAll(); contentRepository.VerifyAll();
} }

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

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