Browse Source

Batch endpoint for graphql.

pull/313/head
Sebastian Stehle 7 years ago
parent
commit
054b91b7e6
  1. 4
      src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
  2. 61
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  4. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs
  5. 16
      src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  6. 39
      src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

4
src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs

@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm
{
public class EdmModelBuilder : CachingProviderBase
{
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
public EdmModelBuilder(IMemoryCache cache)
: base(cache)
{
@ -32,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60);
entry.AbsoluteExpirationRelativeToNow = CacheTime;
return BuildEdmModel(schema.SchemaDef, app.PartitionResolver());
});

61
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -23,7 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private readonly IAssetQueryService assetQuery;
private readonly IAppProvider appProvider;
public CachingGraphQLService(IMemoryCache cache,
public CachingGraphQLService(
IMemoryCache cache,
IAppProvider appProvider,
IAssetQueryService assetQuery,
IContentQueryService contentQuery,
@ -41,39 +42,65 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.urlGenerator = urlGenerator;
}
public async Task<(object Data, object[] Errors)> QueryAsync(QueryContext context, GraphQLQuery query)
public async Task<(bool HasError, object[] Response)> QueryAsync(QueryContext context, GraphQLQuery[] queries)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(queries, nameof(queries));
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q)));
return (result.Any(x => x.HasError), result.Select(x => x.Response).ToArray());
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, GraphQLQuery query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
var result = await QueryInternalAsync(model, ctx, query);
return result;
}
private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query)
{
if (string.IsNullOrWhiteSpace(query.Query))
{
return (new object(), new object[0]);
return (false, new { Data = new object() });
}
var modelContext = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, assetQuery, contentQuery, urlGenerator);
var result = await model.ExecuteAsync(ctx, query);
return await modelContext.ExecuteAsync(ctx, query);
if (result.Errors?.Any() == true)
{
return (false, new { result.Data, result.Errors });
}
else
{
return (false, new { result.Data });
}
}
private async Task<GraphQLModel> GetModelAsync(IAppEntity app)
private Task<GraphQLModel> GetModelAsync(IAppEntity app)
{
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString());
var modelContext = Cache.Get<GraphQLModel>(cacheKey);
if (modelContext == null)
return Cache.GetOrCreateAsync(cacheKey, async entry =>
{
var allSchemas = await appProvider.GetSchemasAsync(app.Id);
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
modelContext = new GraphQLModel(app, allSchemas.Where(x => x.IsPublished), urlGenerator);
Cache.Set(cacheKey, modelContext, CacheDuration);
}
var allSchemas = await appProvider.GetSchemasAsync(app.Id);
return modelContext;
return new GraphQLModel(app, allSchemas, urlGenerator);
});
}
private static object CreateCacheKey(Guid appId, string etag)

2
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
schemasById = schemas.ToDictionary(x => x.Id);
schemasById = schemas.Where(x => x.IsPublished).ToDictionary(x => x.Id);
graphQLSchema = BuildSchema(this);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance);

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs

@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public interface IGraphQLService
{
Task<(object Data, object[] Errors)> QueryAsync(QueryContext context, GraphQLQuery query);
Task<(bool HasError, object[] Response)> QueryAsync(QueryContext context, GraphQLQuery[] queries);
Task<(bool HasError, object[] Response)> QueryAsync(QueryContext context, GraphQLQuery query);
}
}

16
src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -14,7 +14,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker
{
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(10);
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IUsageTracker inner;
public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache)
@ -35,22 +35,18 @@ namespace Squidex.Infrastructure.UsageTracking
return inner.TrackAsync(key, weight, elapsedMs);
}
public async Task<long> GetMonthlyCallsAsync(string key, DateTime date)
public Task<long> GetMonthlyCallsAsync(string key, DateTime date)
{
Guard.NotNull(key, nameof(key));
var cacheKey = string.Concat(key, date);
if (Cache.TryGetValue<long>(cacheKey, out var result))
return Cache.GetOrCreateAsync(cacheKey, entry =>
{
return result;
}
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
result = await inner.GetMonthlyCallsAsync(key, date);
Cache.Set(cacheKey, result, CacheTime);
return result;
return inner.GetMonthlyCallsAsync(key, date);
});
}
}
}

39
src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -50,7 +50,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// GraphQL endpoint.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="query">The graphql endpoint.</param>
/// <param name="query">The graphql query.</param>
/// <returns>
/// 200 => Contents retrieved or mutated.
/// 404 => Schema or app not found.
@ -67,13 +67,44 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
var result = await graphQl.QueryAsync(Context().Base, query);
if (result.Errors?.Length > 0)
if (result.HasError)
{
return BadRequest(new { result.Data, result.Errors });
return BadRequest(result.Response);
}
else
{
return Ok(new { result.Data });
return Ok(result.Response);
}
}
/// <summary>
/// GraphQL endpoint with batch support.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="batch">The graphql queries.</param>
/// <returns>
/// 200 => Contents retrieved or mutated.
/// 404 => Schema or app not found.
/// </returns>
/// <remarks>
/// You can read the generated documentation for your app at /api/content/{appName}/docs
/// </remarks>
[MustBeAppReader]
[HttpGet]
[HttpPost]
[Route("content/{app}/graphql/")]
[ApiCosts(2)]
public async Task<IActionResult> PostGraphQLBatch(string app, [FromBody] GraphQLQuery[] batch)
{
var result = await graphQl.QueryAsync(Context().Base, batch);
if (result.HasError)
{
return BadRequest(result.Response);
}
else
{
return Ok(result.Response);
}
}

Loading…
Cancel
Save