Browse Source

Refactoring/reduce memory usage (#629)

* Reduce memory usage for graphql.

* Added missing files.

* More allocation improvements.

* Removed backup file.
pull/630/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
14da310d14
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  2. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  3. 67
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  4. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  5. 62
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  7. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  8. 54
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  9. 112
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs
  10. 66
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs
  11. 107
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs
  12. 73
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
  13. 21
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs
  14. 85
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs
  15. 200
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs
  16. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataFlatGraphType.cs
  17. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataGraphType.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataInputGraphType.cs
  19. 86
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  20. 91
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs
  21. 32
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs
  22. 42
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs
  23. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs
  24. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentsResultGraphType.cs
  25. 119
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  26. 90
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeFactory.cs
  27. 24
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs
  28. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs
  30. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/Converters.cs
  31. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs
  32. 7
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs
  33. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/GeolocationInputGraphType.cs
  34. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantConverter.cs
  35. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs
  36. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantValueNode.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonConverter.cs
  38. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs
  39. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs
  40. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/NoopGraphType.cs
  41. 124
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs
  42. 5
      backend/src/Squidex/Config/Domain/QueryServices.cs
  43. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

30
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -10,10 +10,8 @@ using System.Linq;
using System.Threading.Tasks;
using GraphQL.Utilities;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Infrastructure;
using Squidex.Log;
@ -41,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, resolver);
var graphQlContext = new GraphQLExecutionContext(context, resolver);
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q)));
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, graphQlContext, q)));
return (result.Any(x => x.HasError), result.Map(x => x.Response));
}
@ -55,21 +53,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var ctx = new GraphQLExecutionContext(context, resolver);
var graphQlContext = new GraphQLExecutionContext(context, resolver);
var result = await QueryInternalAsync(model, ctx, query);
var result = await QueryInternalAsync(model, graphQlContext, query);
return result;
}
private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query)
private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext context, GraphQLQuery query)
{
if (string.IsNullOrWhiteSpace(query.Query))
{
return (false, new { data = new object() });
}
var (data, errors) = await model.ExecuteAsync(ctx, query);
var (data, errors) = await model.ExecuteAsync(context, query);
if (errors?.Any() == true)
{
@ -93,23 +91,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return new GraphQLModel(app,
allSchemas,
GetPageSizeForContents(),
GetPageSizeForAssets(),
resolver.GetRequiredService<IUrlGenerator>(),
resolver.GetRequiredService<GraphQLTypeFactory>(),
resolver.GetRequiredService<ISemanticLog>());
});
}
private int GetPageSizeForContents()
{
return resolver.GetRequiredService<IOptions<ContentOptions>>().Value.DefaultPageSizeGraphQl;
}
private int GetPageSizeForAssets()
{
return resolver.GetRequiredService<IOptions<AssetOptions>>().Value.DefaultPageSizeGraphQl;
}
private static object CreateCacheKey(DomainId appId, string etag)
{
return $"GraphQLModel_{appId}_{etag}";

7
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public ICommandBus CommandBus { get; }
public ISemanticLog Log { get; }
public GraphQLExecutionContext(Context context, IServiceProvider resolver)
: base(context
.WithoutCleanup()
@ -44,6 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
CommandBus = resolver.GetRequiredService<ICommandBus>();
Log = resolver.GetRequiredService<ISemanticLog>();
dataLoaderContextAccessor = resolver.GetRequiredService<IDataLoaderContextAccessor>();
this.resolver = resolver;
@ -54,9 +58,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var loader = resolver.GetRequiredService<DataLoaderDocumentListener>();
execution.Listeners.Add(loader);
execution.FieldMiddleware.Use(Middlewares.Logging(resolver.GetRequiredService<ISemanticLog>()));
execution.FieldMiddleware.Use(Middlewares.Errors());
execution.UserContext = this;
}

67
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -14,7 +14,8 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Log;
@ -26,46 +27,48 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
private static readonly IDocumentExecuter Executor = new DocumentExecuter();
private readonly Dictionary<DomainId, ContentGraphType> contentTypes = new Dictionary<DomainId, ContentGraphType>();
private readonly PartitionResolver partitionResolver;
private readonly IObjectGraphType assetType;
private readonly IGraphType assetListType;
private readonly GraphQLSchema graphQLSchema;
private readonly GraphQLTypeVisitor graphQLTypeVisitor;
private readonly GraphQLTypeFactory graphQLTypeFactory;
private readonly ISemanticLog log;
public bool CanGenerateAssetSourceUrl { get; }
#pragma warning disable IDE0044 // Add readonly modifier
private GraphQLTypeVisitor typeVisitor;
#pragma warning disable IDE0044 // Add readonly modifier
private PartitionResolver partitionResolver;
#pragma warning restore IDE0044 // Add readonly modifier
static GraphQLModel()
{
ValueConverter.Register<string, DomainId>(DomainId.Create);
}
public GraphQLModel(IAppEntity app,
IEnumerable<ISchemaEntity> schemas,
int pageSizeContents,
int pageSizeAssets,
IUrlGenerator urlGenerator, ISemanticLog log)
public GraphQLTypeFactory TypeFactory
{
this.log = log;
get { return graphQLTypeFactory; }
}
partitionResolver = app.PartitionResolver();
public GraphQLModel(IAppEntity app, IEnumerable<ISchemaEntity> schemas, GraphQLTypeFactory typeFactory, ISemanticLog log)
{
graphQLTypeFactory = typeFactory;
CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl;
this.log = log;
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
partitionResolver = app.PartitionResolver();
graphQLTypeVisitor = new GraphQLTypeVisitor(contentTypes, this, assetListType);
typeVisitor = new GraphQLTypeVisitor(contentTypes, this);
var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList();
BuildSchemas(allSchemas);
graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas);
graphQLSchema = BuildSchema(this, allSchemas);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance);
graphQLSchema.RegisterValueConverter(InstantConverter.Instance);
InitializeContentTypes(allSchemas, pageSizeContents);
InitializeContentTypes(allSchemas);
partitionResolver = null!;
typeVisitor = null!;
}
private void BuildSchemas(List<ISchemaEntity> allSchemas)
@ -76,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}
private void InitializeContentTypes(List<ISchemaEntity> allSchemas, int pageSize)
private void InitializeContentTypes(List<ISchemaEntity> allSchemas)
{
var i = 0;
@ -84,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var schema = allSchemas[i];
contentType.Initialize(this, schema, allSchemas, pageSize);
contentType.Initialize(this, schema, allSchemas);
i++;
}
@ -93,19 +96,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
graphQLSchema.RegisterType(contentType);
}
graphQLSchema.Initialize();
graphQLSchema.CleanupMetadata();
}
private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List<ISchemaEntity> schemas)
private static GraphQLSchema BuildSchema(GraphQLModel model, List<ISchemaEntity> schemas)
{
var schema = new GraphQLSchema
{
Query = new AppQueriesGraphType(
model,
pageSizeContents,
pageSizeAssets,
schemas)
Query = new AppQueriesGraphType(model, schemas)
};
schema.RegisterType(ContentInterfaceGraphType.Instance);
var schemasWithFields = schemas.Where(x => x.SchemaDef.Fields.Count > 0);
if (schemasWithFields.Any())
@ -128,12 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{
return field.Accept(graphQLTypeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName));
}
public IGraphType GetAssetType()
{
return assetType;
return field.Accept(typeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName));
}
public IGraphType GetContentType(DomainId schemaId)

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -16,11 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public interface IGraphModel
{
bool CanGenerateAssetSourceUrl { get; }
IFieldPartitioning ResolvePartition(Partitioning key);
IGraphType GetAssetType();
GraphQLTypeFactory TypeFactory { get; }
IGraphType GetContentType(DomainId schemaId);

62
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs

@ -1,62 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL;
using GraphQL.Instrumentation;
using GraphQL.Types;
using Squidex.Infrastructure;
using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public static class Middlewares
{
public static Func<ISchema, FieldMiddlewareDelegate, FieldMiddlewareDelegate> Logging(ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
return (_, next) =>
{
return async context =>
{
try
{
return await next(context);
}
catch (Exception ex)
{
log.LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldName));
throw;
}
};
};
}
public static Func<ISchema, FieldMiddlewareDelegate, FieldMiddlewareDelegate> Errors()
{
return (_, next) =>
{
return async context =>
{
try
{
return await next(context);
}
catch (DomainException ex)
{
throw new ExecutionError(ex.Message);
}
};
};
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -8,7 +8,7 @@
using System;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{

1
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs

@ -7,6 +7,7 @@
using System.Collections.Generic;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Schemas;

54
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs

@ -7,6 +7,7 @@
using System.Collections.Generic;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
@ -14,12 +15,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class AppQueriesGraphType : ObjectGraphType
{
public AppQueriesGraphType(IGraphModel model, int pageSizeContents, int pageSizeAssets, IEnumerable<ISchemaEntity> schemas)
public AppQueriesGraphType(IGraphModel model, IEnumerable<ISchemaEntity> schemas)
{
var assetType = model.GetAssetType();
AddAssetFind(assetType);
AddAssetsQueries(assetType, pageSizeAssets);
AddField(model.TypeFactory.FindAsset);
AddField(model.TypeFactory.QueryAssets);
AddField(model.TypeFactory.QueryAssetsWithTotal);
foreach (var schema in schemas)
{
@ -39,25 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
schemaId,
schemaType,
schemaName,
contentType,
pageSizeContents);
contentType);
}
Description = "The app queries.";
}
private void AddAssetFind(IGraphType assetType)
{
AddField(new FieldType
{
Name = "findAsset",
Arguments = AssetActions.Find.Arguments,
ResolvedType = assetType,
Resolver = AssetActions.Find.Resolver,
Description = "Find an asset by id."
});
}
private void AddContentFind(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType)
{
AddField(new FieldType
@ -70,37 +57,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
});
}
private void AddAssetsQueries(IGraphType assetType, int pageSize)
{
var resolver = AssetActions.Query.Resolver;
AddField(new FieldType
{
Name = "queryAssets",
Arguments = AssetActions.Query.Arguments(pageSize),
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
Resolver = resolver,
Description = "Get assets."
});
AddField(new FieldType
{
Name = "queryAssetsWithTotal",
Arguments = AssetActions.Query.Arguments(pageSize),
ResolvedType = new AssetsResultGraphType(assetType),
Resolver = resolver,
Description = "Get assets and total count."
});
}
private void AddContentQueries(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType, int pageSize)
private void AddContentQueries(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType)
{
var resolver = ContentActions.QueryOrReferencing.Query(schemaId);
AddField(new FieldType
{
Name = $"query{schemaType}Contents",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize),
Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = resolver,
Description = $"Query {schemaName} content items."
@ -109,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = $"query{schemaType}ContentsWithTotal",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize),
Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType),
Resolver = resolver,
Description = $"Query {schemaName} content items with total count."

112
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs

@ -1,112 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class AssetActions
{
public static class Metadata
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "path",
Description = "The path to the json value",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
public static readonly IFieldResolver Resolver = new FuncFieldResolver<IEnrichedAssetEntity, object?>(c =>
{
if (c.Arguments.TryGetValue("path", out var path))
{
c.Source.Metadata.TryGetByPath(path as string, out var result);
return result;
}
return c.Source.Metadata;
});
}
public static class Find
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the asset (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
}
};
public static readonly IFieldResolver Resolver = new FuncFieldResolver<object?>(c =>
{
var assetId = c.GetArgument<DomainId>("id");
return ((GraphQLExecutionContext)c.UserContext).FindAssetAsync(assetId);
});
}
public static class Query
{
private static QueryArguments? resolver;
public static QueryArguments Arguments(int pageSize)
{
return resolver ??= new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "top",
Description = $"Optional number of assets to take (Default: {pageSize}).",
DefaultValue = pageSize,
ResolvedType = AllTypes.Int
},
new QueryArgument(AllTypes.None)
{
Name = "skip",
Description = "Optional number of assets to skip.",
DefaultValue = 0,
ResolvedType = AllTypes.Int
},
new QueryArgument(AllTypes.None)
{
Name = "filter",
Description = "Optional OData filter.",
DefaultValue = null,
ResolvedType = AllTypes.String
},
new QueryArgument(AllTypes.None)
{
Name = "orderby",
Description = "Optional OData order definition.",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
}
public static readonly IFieldResolver Resolver = new FuncFieldResolver<object?>(c =>
{
var query = c.BuildODataQuery();
return ((GraphQLExecutionContext)c.UserContext).QueryAssetsAsync(query);
});
}
}
}

66
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs

@ -1,66 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL;
using GraphQL.Resolvers;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class AssetResolvers
{
public static readonly IFieldResolver Url = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
});
public static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion);
});
public static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
});
public static readonly IFieldResolver FileHash = Resolve(x => x.FileHash);
public static readonly IFieldResolver FileName = Resolve(x => x.FileName);
public static readonly IFieldResolver FileSize = Resolve(x => x.FileSize);
public static readonly IFieldResolver FileType = Resolve(x => x.FileName.FileType());
public static readonly IFieldResolver FileVersion = Resolve(x => x.FileVersion);
public static readonly IFieldResolver IsImage = Resolve(x => x.Type == AssetType.Image);
public static readonly IFieldResolver IsProtected = Resolve(x => x.IsProtected);
public static readonly IFieldResolver ListTotal = ResolveList(x => x.Total);
public static readonly IFieldResolver ListItems = ResolveList(x => x);
public static readonly IFieldResolver MetadataText = Resolve(x => x.MetadataText);
public static readonly IFieldResolver MimeType = Resolve(x => x.MimeType);
public static readonly IFieldResolver PixelHeight = Resolve(x => x.Metadata.GetPixelHeight());
public static readonly IFieldResolver PixelWidth = Resolve(x => x.Metadata.GetPixelWidth());
public static readonly IFieldResolver Slug = Resolve(x => x.Slug);
public static readonly IFieldResolver Tags = Resolve(x => x.TagNames);
public static readonly IFieldResolver Type = Resolve(x => x.Type);
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext));
}
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, T> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source));
}
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedAssetEntity>, T> action)
{
return new FuncFieldResolver<IResultList<IEnrichedAssetEntity>, object?>(c => action(c.Source));
}
}
}

107
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs

@ -0,0 +1,107 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
{
internal static class AssetActions
{
public static class Metadata
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "path",
Description = "The path to the json value",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
public static readonly IFieldResolver Resolver = Resolvers.Sync<IEnrichedAssetEntity, object?>((source, fieldContext, _) =>
{
if (fieldContext.Arguments.TryGetValue("path", out var path))
{
source.Metadata.TryGetByPath(path as string, out var result);
return result;
}
return source.Metadata;
});
}
public static class Find
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the asset (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
}
};
public static readonly IFieldResolver Resolver = Resolvers.Async<object, object?>(async (_, fieldContext, context) =>
{
var assetId = fieldContext.GetArgument<DomainId>("id");
return await context.FindAssetAsync(assetId);
});
}
public static class Query
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "top",
Description = $"Optional number of assets to take.",
DefaultValue = null,
ResolvedType = AllTypes.Int
},
new QueryArgument(AllTypes.None)
{
Name = "skip",
Description = "Optional number of assets to skip.",
DefaultValue = 0,
ResolvedType = AllTypes.Int
},
new QueryArgument(AllTypes.None)
{
Name = "filter",
Description = "Optional OData filter.",
DefaultValue = null,
ResolvedType = AllTypes.String
},
new QueryArgument(AllTypes.None)
{
Name = "orderby",
Description = "Optional OData order definition.",
DefaultValue = null,
ResolvedType = AllTypes.String
}
};
public static readonly IFieldResolver Resolver = Resolvers.Async<object, object>(async (_, fieldContext, context) =>
{
var query = fieldContext.BuildODataQuery();
return await context.QueryAssetsAsync(query);
});
}
}
}

73
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs

@ -5,14 +5,20 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
{
public sealed class AssetGraphType : ObjectGraphType<IEnrichedAssetEntity>
internal sealed class AssetGraphType : ObjectGraphType<IEnrichedAssetEntity>
{
public AssetGraphType(IGraphModel model)
public AssetGraphType(bool canGenerateSourceUrl)
{
Name = "Asset";
@ -68,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "mimeType",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.MimeType,
Resolver = Resolve(x => x.MimeType),
Description = "The mime type."
});
@ -76,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "url",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.Url,
Resolver = Url,
Description = "The url to the asset."
});
@ -84,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "thumbnailUrl",
ResolvedType = AllTypes.String,
Resolver = AssetResolvers.ThumbnailUrl,
Resolver = ThumbnailUrl,
Description = "The thumbnail url to the asset."
});
@ -92,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileName",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileName,
Resolver = Resolve(x => x.FileName),
Description = "The file name."
});
@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileHash",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileHash,
Resolver = Resolve(x => x.FileHash),
Description = "The hash of the file. Can be null for old files."
});
@ -108,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileType",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileType,
Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type."
});
@ -116,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileSize",
ResolvedType = AllTypes.NonNullInt,
Resolver = AssetResolvers.FileSize,
Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes."
});
@ -124,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileVersion",
ResolvedType = AllTypes.NonNullInt,
Resolver = AssetResolvers.FileVersion,
Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file."
});
@ -132,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "slug",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.Slug,
Resolver = Resolve(x => x.Slug),
Description = "The file name as slug."
});
@ -140,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "isProtected",
ResolvedType = AllTypes.NonNullBoolean,
Resolver = AssetResolvers.IsProtected,
Resolver = Resolve(x => x.IsProtected),
Description = "True, when the asset is not public."
});
@ -148,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean,
Resolver = AssetResolvers.IsImage,
Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines if the uploaded file is an image.",
DeprecationReason = "Use 'type' field instead."
});
@ -157,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "pixelWidth",
ResolvedType = AllTypes.Int,
Resolver = AssetResolvers.PixelWidth,
Resolver = Resolve(x => x.Metadata.GetPixelWidth()),
Description = "The width of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
});
@ -166,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "pixelHeight",
ResolvedType = AllTypes.Int,
Resolver = AssetResolvers.PixelHeight,
Resolver = Resolve(x => x.Metadata.GetPixelHeight()),
Description = "The height of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead."
});
@ -175,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "type",
ResolvedType = AllTypes.NonNullAssetType,
Resolver = AssetResolvers.Type,
Resolver = Resolve(x => x.Type),
Description = "The type of the image."
});
@ -183,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "metadataText",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.MetadataText,
Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata."
});
@ -191,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "tags",
ResolvedType = null,
Resolver = AssetResolvers.Tags,
Resolver = Resolve(x => x.TagNames),
Description = "The asset tags.",
Type = AllTypes.NonNullTagsType
});
@ -205,18 +211,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The asset metadata."
});
if (model.CanGenerateAssetSourceUrl)
if (canGenerateSourceUrl)
{
AddField(new FieldType
{
Name = "sourceUrl",
ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.SourceUrl,
Resolver = SourceUrl,
Description = "The source url of the asset."
});
}
Description = "An asset";
}
private static readonly IFieldResolver Url = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
});
private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion);
});
private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
});
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver)
{
return Resolvers.Sync(resolver);
}
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, T> resolver)
{
return Resolvers.Sync(resolver);
}
}
}

21
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs

@ -5,35 +5,42 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets
{
public sealed class AssetsResultGraphType : ObjectGraphType<IResultList<IAssetEntity>>
internal sealed class AssetsResultGraphType : ObjectGraphType<IResultList<IAssetEntity>>
{
public AssetsResultGraphType(IGraphType assetType)
public AssetsResultGraphType(IGraphType assetsList)
{
Name = "AssetResultDto";
AddField(new FieldType
{
Name = "total",
ResolvedType = AllTypes.Int,
Resolver = AssetResolvers.ListTotal,
ResolvedType = AllTypes.NonNullInt,
Resolver = ResolveList(x => x.Total),
Description = "The total count of assets."
});
AddField(new FieldType
{
Name = "items",
Resolver = AssetResolvers.ListItems,
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
ResolvedType = assetsList,
Resolver = ResolveList(x => x),
Description = "The assets."
});
Description = "List of assets and total count of assets.";
}
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedAssetEntity>, T> resolver)
{
return Resolvers.Sync(resolver);
}
}
}

85
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs

@ -1,85 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedContentEntity>
{
public ContentInterfaceGraphType()
{
Name = "Content";
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the content."
});
AddField(new FieldType
{
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version,
Description = "The version of the content."
});
AddField(new FieldType
{
Name = "created",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.Created,
Description = "The date and time when the content has been created."
});
AddField(new FieldType
{
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the content."
});
AddField(new FieldType
{
Name = "lastModified",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.LastModified,
Description = "The date and time when the content has been modified last."
});
AddField(new FieldType
{
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the content last."
});
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Status,
Description = "The the status of the content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.StatusColor,
Description = "The color status of the content."
});
Description = "The structure of all content types.";
}
}
}

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

@ -14,16 +14,48 @@ using GraphQL.Types;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static class ContentActions
{
private static readonly QueryArgument Id = new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
};
private static readonly QueryArgument NewId = new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
};
private static readonly QueryArgument ExpectedVersion = new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
};
private static readonly QueryArgument Publish = new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
};
public static class Json
{
public static readonly QueryArguments Arguments = new QueryArguments
@ -37,9 +69,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
};
public static readonly ValueResolver Resolver = (value, c) =>
public static readonly ValueResolver Resolver = (value, fieldContext, context) =>
{
if (c.Arguments.TryGetValue("path", out var p) && p is string path)
if (fieldContext.Arguments.TryGetValue("path", out var p) && p is string path)
{
value.TryGetByPath(path, out var result);
@ -65,13 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
Id,
new QueryArgument(AllTypes.None)
{
Name = "version",
@ -85,19 +111,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var schemaIdValue = schemaId.ToString();
return new FuncFieldResolver<object?>(c =>
return Resolvers.Async<object, object?>(async (_, fieldContext, context) =>
{
var contentId = c.GetArgument<DomainId>("id");
var contentId = fieldContext.GetArgument<DomainId>("id");
var version = c.GetArgument<int?>("version");
var version = fieldContext.GetArgument<int?>("version");
if (version >= 0)
{
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(schemaIdValue, contentId, version.Value);
return await context.FindContentAsync(schemaIdValue, contentId, version.Value);
}
else
{
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId);
return await context.FindContentAsync(contentId);
}
});
}
@ -105,17 +131,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static class QueryOrReferencing
{
private static QueryArguments? arguments;
public static QueryArguments Arguments(int pageSize)
{
return arguments ??= new QueryArguments
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "top",
Description = $"Optional number of contents to take (Default: {pageSize}).",
DefaultValue = pageSize,
Description = $"Optional number of contents to take.",
DefaultValue = null,
ResolvedType = AllTypes.Int
},
new QueryArgument(AllTypes.None)
@ -147,17 +169,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
ResolvedType = AllTypes.String
}
};
}
public static IFieldResolver Query(DomainId schemaId)
{
var schemaIdValue = schemaId.ToString();
return new FuncFieldResolver<object?>(c =>
return Resolvers.Async<object, object>(async (_, fieldContext, context) =>
{
var query = c.BuildODataQuery();
var query = fieldContext.BuildODataQuery();
return ((GraphQLExecutionContext)c.UserContext).QueryContentsAsync(schemaIdValue, query);
return await context.QueryContentsAsync(schemaIdValue, query);
});
}
@ -165,13 +186,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var schemaIdValue = schemaId.ToString();
return new FuncFieldResolver<IContentEntity, object?>(c =>
return Resolvers.Async<IContentEntity, object?>(async (source, fieldContext, context) =>
{
var query = c.BuildODataQuery();
var query = fieldContext.BuildODataQuery();
var contentId = c.Source.Id;
var contentId = source.Id;
return ((GraphQLExecutionContext)c.UserContext).QueryReferencingContentsAsync(schemaIdValue, query, c.Source.Id);
return await context.QueryReferencingContentsAsync(schemaIdValue, query, source.Id);
});
}
}
@ -189,20 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The optional custom content id.",
DefaultValue = null,
ResolvedType = AllTypes.String
}
Publish, NewId
};
}
@ -234,13 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = null,
ResolvedType = AllTypes.NonNullString
},
Id,
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -248,20 +250,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{
Name = "publish",
Description = "Set to true to autopublish content on create.",
DefaultValue = false,
ResolvedType = AllTypes.Boolean
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
Publish,
ExpectedVersion
};
}
@ -286,13 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
Id,
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -300,13 +284,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
ExpectedVersion
};
}
@ -328,13 +306,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
return new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
Id,
new QueryArgument(AllTypes.None)
{
Name = "data",
@ -342,13 +314,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType)
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
ExpectedVersion
};
}
@ -368,13 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = null,
ResolvedType = AllTypes.NonNullString
},
Id,
new QueryArgument(AllTypes.None)
{
Name = "status",
@ -389,13 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null,
ResolvedType = AllTypes.Date
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
ExpectedVersion
};
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
@ -415,20 +369,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = null,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
Id,
ExpectedVersion
};
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId)
@ -451,31 +393,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
private static IFieldResolver ResolveAsync<T>(NamedId<DomainId> appId, NamedId<DomainId> schemaId, Func<IResolveFieldContext, ContentCommand> action)
{
return new FuncFieldResolver<Task<T>>(async c =>
return Resolvers.Async<object, T>(async (source, fieldContext, context) =>
{
var e = (GraphQLExecutionContext)c.UserContext;
try
{
var command = action(c);
var command = action(fieldContext);
command.AppId = appId;
command.SchemaId = schemaId;
command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any);
command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any);
var commandContext = await e.CommandBus.PublishAsync(command);
var commandContext = await context.CommandBus.PublishAsync(command);
return commandContext.Result<T>();
}
catch (ValidationException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
fieldContext.Errors.Add(new ExecutionError(ex.Message));
throw;
}
catch (DomainException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
fieldContext.Errors.Add(new ExecutionError(ex.Message));
throw;
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataFlatGraphType.cs

@ -9,7 +9,7 @@ using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentDataFlatGraphType : ObjectGraphType<FlatContentData>
{
@ -27,8 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = fieldName,
Arguments = args,
Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name),
ResolvedType = resolvedType,
Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name),
Description = field.RawProperties.Hints
});
}

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataGraphType.cs

@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentDataGraphType : ObjectGraphType<ContentData>
{
@ -39,8 +39,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = partitionKey.EscapePartition(),
Arguments = args,
Resolver = ContentResolvers.Partition(valueResolver, partitionKey),
ResolvedType = resolvedType,
Resolver = ContentResolvers.Partition(valueResolver, partitionKey),
Description = field.RawProperties.Hints
});
}
@ -50,8 +50,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = fieldName,
Resolver = ContentResolvers.Field(field),
ResolvedType = fieldGraphType,
Resolver = ContentResolvers.Field(field),
Description = $"The {displayName} field."
});
}

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataInputGraphType.cs

@ -10,7 +10,7 @@ using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentDataInputGraphType : InputObjectGraphType
{
@ -38,8 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
fieldGraphType.AddField(new FieldType
{
Name = partitionKey.EscapePartition(),
Resolver = null,
ResolvedType = resolvedType,
Resolver = null,
Description = field.RawProperties.Hints
}).WithSourceName(partitionKey);
}
@ -49,8 +49,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = fieldName,
Resolver = null,
ResolvedType = fieldGraphType,
Resolver = null,
Description = $"The {displayName} field."
}).WithSourceName(field.Name);
}

86
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static class ContentFields
{
public static readonly FieldType Id = new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = "The id of the content."
};
public static readonly FieldType Version = new FieldType
{
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version,
Description = "The version of the content."
};
public static readonly FieldType Created = new FieldType
{
Name = "created",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.Created,
Description = "The date and time when the content has been created."
};
public static readonly FieldType CreatedBy = new FieldType
{
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy,
Description = "The user that has created the content."
};
public static readonly FieldType LastModified = new FieldType
{
Name = "lastModified",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.LastModified,
Description = "The date and time when the content has been modified last."
};
public static readonly FieldType LastModifiedBy = new FieldType
{
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy,
Description = "The user that has updated the content last."
};
public static readonly FieldType Status = new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.ToString().ToUpperInvariant()),
Description = "The the status of the content."
};
public static readonly FieldType StatusColor = new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = "The color status of the content."
};
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver)
{
return Resolvers.Sync(resolver);
}
}
}

91
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs

@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{
@ -27,71 +27,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = schemaType.SafeTypeName();
AddField(new FieldType
{
Name = "id",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.Id,
Description = $"The id of the {schemaName} content."
});
AddField(new FieldType
{
Name = "version",
ResolvedType = AllTypes.NonNullInt,
Resolver = EntityResolvers.Version,
Description = $"The version of the {schemaName} content."
});
AddField(new FieldType
{
Name = "created",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.Created,
Description = $"The date and time when the {schemaName} content has been created."
});
AddField(new FieldType
{
Name = "createdBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.CreatedBy,
Description = $"The user that has created the {schemaName} content."
});
AddField(new FieldType
{
Name = "lastModified",
ResolvedType = AllTypes.NonNullDate,
Resolver = EntityResolvers.LastModified,
Description = $"The date and time when the {schemaName} content has been modified last."
});
AddField(new FieldType
{
Name = "lastModifiedBy",
ResolvedType = AllTypes.NonNullString,
Resolver = EntityResolvers.LastModifiedBy,
Description = $"The user that has updated the {schemaName} content last."
});
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Status,
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.StatusColor,
Description = $"The color status of the {schemaName} content."
});
AddField(ContentFields.Id);
AddField(ContentFields.Version);
AddField(ContentFields.Created);
AddField(ContentFields.CreatedBy);
AddField(ContentFields.LastModified);
AddField(ContentFields.LastModifiedBy);
AddField(ContentFields.Status);
AddField(ContentFields.StatusColor);
Interface<ContentInterfaceGraphType>();
AddResolvedInterface(ContentInterfaceGraphType.Instance);
Description = $"The structure of a {schemaName} content type.";
@ -103,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return value is IContentEntity content && content.SchemaId?.Id == schemaId;
}
public void Initialize(IGraphModel model, ISchemaEntity schema, IEnumerable<ISchemaEntity> all, int pageSize)
public void Initialize(IGraphModel model, ISchemaEntity schema, IEnumerable<ISchemaEntity> all)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
@ -113,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "url",
ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Url,
Description = $"The url to the the {schemaName} content."
Description = $"The url to the content."
});
var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model);
@ -125,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "data",
ResolvedType = new NonNullGraphType(contentDataType),
Resolver = ContentResolvers.Data,
Description = $"The data of the {schemaName} content."
Description = $"The data of the content."
});
}
@ -138,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "flatData",
ResolvedType = new NonNullGraphType(contentDataTypeFlat),
Resolver = ContentResolvers.FlatData,
Description = $"The flat data of the {schemaName} content."
Description = $"The flat data of the content."
});
}
@ -150,18 +95,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var contentType = model.GetContentType(referencingId);
AddReferencingQueries(referencingId, referencingType, referencingName, contentType, pageSize);
AddReferencingQueries(referencingId, referencingType, referencingName, contentType);
}
}
private void AddReferencingQueries(DomainId referencingId, string referencingType, string referencingName, IGraphType contentType, int pageSize)
private void AddReferencingQueries(DomainId referencingId, string referencingType, string referencingName, IGraphType contentType)
{
var resolver = ContentActions.QueryOrReferencing.Referencing(referencingId);
AddField(new FieldType
{
Name = $"referencing{referencingType}Contents",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize),
Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = resolver,
Description = $"Query {referencingName} content items."
@ -170,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = $"referencing{referencingType}ContentsWithTotal",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize),
Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ContentsResultGraphType(referencingType, referencingName, contentType),
Resolver = resolver,
Description = $"Query {referencingName} content items with total count."

32
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
internal sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedContentEntity>
{
public static readonly IInterfaceGraphType Instance = new ContentInterfaceGraphType();
private ContentInterfaceGraphType()
{
Name = "Content";
AddField(ContentFields.Id);
AddField(ContentFields.Version);
AddField(ContentFields.Created);
AddField(ContentFields.CreatedBy);
AddField(ContentFields.LastModified);
AddField(ContentFields.LastModifiedBy);
AddField(ContentFields.Status);
AddField(ContentFields.StatusColor);
Description = "The structure of all content types.";
}
}
}

42
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs

@ -15,17 +15,17 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public static class ContentResolvers
internal static class ContentResolvers
{
public static IFieldResolver NestedValue(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<JsonObject, object?>(c =>
return Resolvers.Sync<JsonObject, object?>((source, fieldContext, context) =>
{
if (c.Source.TryGetValue(key, out var value))
if (source.TryGetValue(key, out var value))
{
return valueResolver(value, c);
return valueResolver(value, fieldContext, context);
}
return null;
@ -34,11 +34,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static IFieldResolver Partition(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<ContentFieldData, object?>(c =>
return Resolvers.Sync<ContentFieldData, object?>((source, fieldContext, context) =>
{
if (c.Source.TryGetValue(key, out var value) && value != null)
if (source.TryGetValue(key, out var value) && value != null)
{
return valueResolver(value, c);
return valueResolver(value, fieldContext, context);
}
return null;
@ -47,11 +47,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static IFieldResolver FlatPartition(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<FlatContentData, object?>(c =>
return Resolvers.Sync<FlatContentData, object?>((source, fieldContext, context) =>
{
if (c.Source.TryGetValue(key, out var value) && value != null)
if (source.TryGetValue(key, out var value) && value != null)
{
return valueResolver(value, c);
return valueResolver(value, fieldContext, context);
}
return null;
@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
var fieldName = field.Name;
return new FuncFieldResolver<ContentData, IReadOnlyDictionary<string, IJsonValue>?>(c =>
return Resolvers.Sync<ContentData, IReadOnlyDictionary<string, IJsonValue>?>(source =>
{
return c.Source?.GetOrDefault(fieldName);
return source?.GetOrDefault(fieldName);
});
}
@ -83,24 +83,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
});
public static readonly IFieldResolver Data = Resolve(x => x.Data);
public static readonly IFieldResolver Status = Resolve(x => x.Status.Name.ToUpperInvariant());
public static readonly IFieldResolver StatusColor = Resolve(x => x.StatusColor);
public static readonly IFieldResolver ListTotal = ResolveList(x => x.Total);
public static readonly IFieldResolver ListItems = ResolveList(x => x);
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, IResolveFieldContext, GraphQLExecutionContext, T> action)
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, IResolveFieldContext, GraphQLExecutionContext, T> resolver)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext));
return Resolvers.Sync(resolver);
}
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> action)
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> resolver)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source));
return Resolvers.Sync(resolver);
}
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedContentEntity>, T> action)
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedContentEntity>, T> resolver)
{
return new FuncFieldResolver<IResultList<IEnrichedContentEntity>, object?>(c => action(c.Source));
return Resolvers.Sync(resolver);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs

@ -10,7 +10,7 @@ using System.Linq;
using GraphQL.Types;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentUnionGraphType : UnionGraphType
{

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentsResultGraphType.cs

@ -8,7 +8,7 @@
using GraphQL.Types;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents
{
public sealed class ContentsResultGraphType : ObjectGraphType<IResultList<IContentEntity>>
{
@ -19,16 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "total",
Resolver = ContentResolvers.ListTotal,
ResolvedType = AllTypes.NonNullInt,
Resolver = ContentResolvers.ListTotal,
Description = $"The total number of {schemaName} items."
});
AddField(new FieldType
{
Name = "items",
Resolver = ContentResolvers.ListItems,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = ContentResolvers.ListItems,
Description = $"The {schemaName} items."
});

119
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs

@ -10,9 +10,12 @@ using System.Collections.Generic;
using System.Linq;
using GraphQL;
using GraphQL.Types;
using GraphQL.Utilities;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.ObjectPool;
using Squidex.Text;
using GraphQLSchema = GraphQL.Types.Schema;
#pragma warning disable RECS0015 // If an extension method is called as static method convert it to method syntax
@ -63,25 +66,53 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static string BuildODataQuery(this IResolveFieldContext context)
{
var odataQuery = "?" +
string.Join("&",
context.Arguments
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value))
.Select(x => $"${x.Key}={x.Value}"));
var sb = DefaultPools.StringBuilder.Get();
try
{
sb.Append('?');
foreach (var argument in context.Arguments)
{
var value = argument.Value?.ToString();
if (!string.IsNullOrWhiteSpace(value))
{
if (sb.Length > 1)
{
sb.Append('&');
}
return odataQuery;
sb.Append('$');
sb.Append(argument.Key);
sb.Append('=');
sb.Append(value);
}
}
return sb.ToString();
}
finally
{
DefaultPools.StringBuilder.Return(sb);
}
}
public static FieldType WithSourceName(this FieldType field, object value)
{
field.Metadata["sourceName"] = value;
if (field is MetadataProvider metadataProvider)
{
metadataProvider.Metadata = new Dictionary<string, object>
{
["sourceName"] = value
};
}
return field;
}
public static string GetSourceName(this FieldType field)
{
return field.Metadata.GetOrAddDefault("sourceName") as string ?? field.Name;
return field.GetMetadata("sourceName", string.Empty);
}
public static IGraphType Flatten(this QueryArgument type)
@ -98,5 +129,77 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return type;
}
public static void CleanupMetadata(this GraphQLSchema schema)
{
var targets = new HashSet<IProvideMetadata>(ReferenceEqualityComparer.Instance);
foreach (var type in schema.AllTypes)
{
FindTargets(type, targets);
}
foreach (var target in targets.OfType<MetadataProvider>())
{
var metadata = target.Metadata;
if (metadata != null && metadata.Count == 0)
{
target.Metadata = null;
}
}
}
private static void FindTargets(IGraphType type, HashSet<IProvideMetadata> targets)
{
if (type == null)
{
return;
}
if (targets.Add(type))
{
if (type is IComplexGraphType complexType)
{
foreach (var field in complexType.Fields)
{
targets.Add(field);
FindTargets(field.ResolvedType, targets);
if (field.Arguments != null)
{
foreach (var argument in field.Arguments)
{
targets.Add(argument);
FindTargets(argument.ResolvedType, targets);
}
}
}
if (type is IObjectGraphType objectGraphType && objectGraphType.ResolvedInterfaces != null)
{
foreach (var @interface in objectGraphType.ResolvedInterfaces)
{
FindTargets(@interface, targets);
}
}
if (type is IAbstractGraphType abstractGraphType && abstractGraphType.PossibleTypes != null)
{
foreach (var possibleType in abstractGraphType.PossibleTypes)
{
FindTargets(possibleType, targets);
}
}
}
if (type is IProvideResolvedType provideType)
{
FindTargets(provideType.ResolvedType, targets);
}
}
}
}
}

90
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeFactory.cs

@ -0,0 +1,90 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public class GraphQLTypeFactory
{
private readonly Lazy<IGraphType> asset;
private readonly Lazy<IGraphType> assetsList;
private readonly Lazy<IGraphType> assetsResult;
private readonly Lazy<FieldType> findAsset;
private readonly Lazy<FieldType> queryAssets;
private readonly Lazy<FieldType> queryAssetsWithTotal;
public IGraphType Asset => asset.Value;
public IGraphType AssetsList => assetsList.Value;
public IGraphType AssetsResult => assetsResult.Value;
public FieldType FindAsset => findAsset.Value;
public FieldType QueryAssets => queryAssets.Value;
public FieldType QueryAssetsWithTotal => queryAssetsWithTotal.Value;
public GraphQLTypeFactory(IUrlGenerator urlGenerator)
{
asset = new Lazy<IGraphType>(() =>
{
return new AssetGraphType(urlGenerator.CanGenerateAssetSourceUrl);
});
assetsList = new Lazy<IGraphType>(() =>
{
return new NonNullGraphType(new ListGraphType(new NonNullGraphType(Asset)));
});
assetsResult = new Lazy<IGraphType>(() =>
{
return new AssetsResultGraphType(AssetsList);
});
findAsset = new Lazy<FieldType>(() =>
{
return new FieldType
{
Name = "findAsset",
Arguments = AssetActions.Find.Arguments,
ResolvedType = Asset,
Resolver = AssetActions.Find.Resolver,
Description = "Find an asset by id."
};
});
queryAssets = new Lazy<FieldType>(() =>
{
return new FieldType
{
Name = "queryAssets",
Arguments = AssetActions.Query.Arguments,
ResolvedType = AssetsList,
Resolver = AssetActions.Query.Resolver,
Description = "Get assets."
};
});
queryAssetsWithTotal = new Lazy<FieldType>(() =>
{
return new FieldType
{
Name = "queryAssetsWithTotal",
Arguments = AssetActions.Query.Arguments,
ResolvedType = AssetsResult,
Resolver = AssetActions.Query.Resolver,
Description = "Get assets and total count."
};
});
}
}
}

24
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs

@ -10,19 +10,20 @@ using System.Linq;
using GraphQL;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public delegate object ValueResolver(IJsonValue value, IResolveFieldContext context);
public delegate object ValueResolver(IJsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context);
internal sealed class GraphQLTypeVisitor : IFieldVisitor<(IGraphType?, ValueResolver?, QueryArguments?), GraphQLTypeVisitor.Args>
{
private static readonly ValueResolver NoopResolver = (value, c) => value;
private static readonly ValueResolver NoopResolver = (value, fieldContext, contex) => value;
private readonly Dictionary<DomainId, ContentGraphType> schemaTypes;
private readonly IGraphType assetListType;
private readonly IGraphModel model;
public readonly struct Args
@ -38,13 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}
}
public GraphQLTypeVisitor(
Dictionary<DomainId, ContentGraphType> schemaTypes,
IGraphModel model,
IGraphType assetListType)
public GraphQLTypeVisitor(Dictionary<DomainId, ContentGraphType> schemaTypes, IGraphModel model)
{
this.assetListType = assetListType;
this.model = model;
this.schemaTypes = schemaTypes;
}
@ -115,14 +113,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets()
{
var resolver = new ValueResolver((value, c) =>
var resolver = new ValueResolver((value, _, context) =>
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.GetReferencedAssetsAsync(value);
});
return (assetListType, resolver, null);
return (model.TypeFactory.AssetsList, resolver, null);
}
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField<ReferencesFieldProperties> field, Args args)
@ -141,10 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
contentType = union;
}
var resolver = new ValueResolver((value, c) =>
var resolver = new ValueResolver((value, _, context) =>
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.GetReferencedContentsAsync(value);
});

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs

@ -8,6 +8,7 @@
using System.Linq;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Json.Objects;
@ -30,14 +31,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
if (resolvedType != null && valueResolver != null)
{
var resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name);
AddField(new FieldType
{
Name = nestedName,
Arguments = args,
ResolvedType = resolvedType,
Resolver = resolver,
Resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name),
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."
});
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs

@ -32,8 +32,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = nestedName,
Resolver = null,
ResolvedType = resolvedType,
Resolver = null,
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."
}).WithSourceName(nestedField.Name);
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/Converters.cs

@ -10,9 +10,9 @@ using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public static class Converters
internal static class Converters
{
public static ContentData ToContentData(this IDictionary<string, object> source, IComplexGraphType type)
{

8
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs

@ -8,9 +8,9 @@
using System;
using GraphQL.Resolvers;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public static class EntityResolvers
internal static class EntityResolvers
{
public static readonly IFieldResolver Id = Resolve<IEntity>(x => x.Id.ToString());
public static readonly IFieldResolver Created = Resolve<IEntity>(x => x.Created);
@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IFieldResolver LastModifiedBy = Resolve<IEntityWithLastModifiedBy>(x => x.LastModifiedBy.ToString());
public static readonly IFieldResolver Version = Resolve<IEntityWithVersion>(x => x.Version);
private static IFieldResolver Resolve<TSource>(Func<TSource, object> action)
private static IFieldResolver Resolve<TSource>(Func<TSource, object> resolver)
{
return new FuncFieldResolver<TSource, object?>(c => action(c.Source));
return Resolvers.Sync(resolver);
}
}
}

7
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class EntitySavedGraphType : ObjectGraphType<EntitySavedResult>
internal sealed class EntitySavedGraphType : ObjectGraphType<EntitySavedResult>
{
public static readonly IGraphType Nullable = new EntitySavedGraphType();
@ -34,10 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
private static IFieldResolver ResolveVersion()
{
return new FuncFieldResolver<EntitySavedResult, long>(x =>
{
return x.Source.Version;
});
return Resolvers.Sync<EntitySavedResult, long>(x => x.Version);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/GeolocationInputGraphType.cs

@ -9,7 +9,7 @@ using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
{
public sealed class GeolocationInputGraphType : InputObjectGraphType
internal sealed class GeolocationInputGraphType : InputObjectGraphType
{
public static readonly IGraphType Nullable = new GeolocationInputGraphType();

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantConverter.cs

@ -9,9 +9,9 @@ using GraphQL.Language.AST;
using GraphQL.Types;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class InstantConverter : IAstFromValueConverter
internal sealed class InstantConverter : IAstFromValueConverter
{
public static readonly InstantConverter Instance = new InstantConverter();

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs

@ -9,9 +9,9 @@ using GraphQL.Language.AST;
using GraphQL.Types;
using NodaTime.Text;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class InstantGraphType : DateGraphType
internal sealed class InstantGraphType : DateGraphType
{
public override object Serialize(object value)
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantValueNode.cs

@ -8,9 +8,9 @@
using GraphQL.Language.AST;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class InstantValueNode : ValueNode<Instant>
internal sealed class InstantValueNode : ValueNode<Instant>
{
public InstantValueNode(Instant value)
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonConverter.cs

@ -10,9 +10,9 @@ using GraphQL.Language.AST;
using GraphQL.Types;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class JsonConverter : IAstFromValueConverter
internal sealed class JsonConverter : IAstFromValueConverter
{
public static readonly JsonConverter Instance = new JsonConverter();

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs

@ -8,9 +8,9 @@
using GraphQL.Language.AST;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class JsonGraphType : ScalarGraphType
internal sealed class JsonGraphType : ScalarGraphType
{
public JsonGraphType()
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs

@ -8,9 +8,9 @@
using GraphQL.Language.AST;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class JsonValueNode : ValueNode<IJsonValue>
internal sealed class JsonValueNode : ValueNode<IJsonValue>
{
public JsonValueNode(IJsonValue value)
{

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs → backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/NoopGraphType.cs

@ -8,9 +8,9 @@
using GraphQL.Language.AST;
using GraphQL.Types;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives
{
public sealed class NoopGraphType : ScalarGraphType
internal sealed class NoopGraphType : ScalarGraphType
{
public NoopGraphType(string name)
{

124
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs

@ -0,0 +1,124 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Resolvers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
using Squidex.Log;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class Resolvers
{
public static IFieldResolver Sync<TSource, T>(Func<TSource, T> resolver)
{
return new SyncResolver<TSource, T>((source, context, execution) => resolver(source));
}
public static IFieldResolver Sync<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver)
{
return new SyncResolver<TSource, T>(resolver);
}
public static IFieldResolver Async<TSource, T>(Func<TSource, Task<T>> resolver)
{
return new AsyncResolver<TSource, T>((source, context, execution) => resolver(source));
}
public static IFieldResolver Async<TSource, T>(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, Task<T>> resolver)
{
return new AsyncResolver<TSource, T>(resolver);
}
private sealed class SyncResolver<TSource, T> : IFieldResolver<T>, IFieldResolver
{
private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver;
public SyncResolver(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, T> resolver)
{
this.resolver = resolver;
}
public T Resolve(IResolveFieldContext context)
{
var executionContext = (GraphQLExecutionContext)context.UserContext;
try
{
return resolver((TSource)context.Source, context, executionContext);
}
catch (ValidationException ex)
{
throw new ExecutionError(ex.Message);
}
catch (DomainException ex)
{
throw new ExecutionError(ex.Message);
}
catch (Exception ex)
{
executionContext.Log.LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldName));
throw;
}
}
object IFieldResolver.Resolve(IResolveFieldContext context)
{
return Resolve(context)!;
}
}
private sealed class AsyncResolver<TSource, T> : IFieldResolver<Task<T>>, IFieldResolver
{
private readonly Func<TSource, IResolveFieldContext, GraphQLExecutionContext, Task<T>> resolver;
public AsyncResolver(Func<TSource, IResolveFieldContext, GraphQLExecutionContext, Task<T>> resolver)
{
this.resolver = resolver;
}
public async Task<T> Resolve(IResolveFieldContext context)
{
var executionContext = (GraphQLExecutionContext)context.UserContext;
try
{
return await resolver((TSource)context.Source, context, executionContext);
}
catch (ValidationException ex)
{
throw new ExecutionError(ex.Message);
}
catch (DomainException ex)
{
throw new ExecutionError(ex.Message);
}
catch (Exception ex)
{
executionContext.Log.LogWarning(ex, w => w
.WriteProperty("action", "resolveField")
.WriteProperty("status", "failed")
.WriteProperty("field", context.FieldName));
throw;
}
}
object IFieldResolver.Resolve(IResolveFieldContext context)
{
return Resolve(context)!;
}
}
}
}

5
backend/src/Squidex/Config/Domain/QueryServices.cs

@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Web.Services;
namespace Squidex.Config.Domain
@ -29,8 +30,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<DataLoaderDocumentListener>()
.AsSelf();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();
services.AddSingletonAs<GraphQLTypeFactory>()
.AsSelf();
services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>();

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
@ -157,6 +158,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var dataLoaderContext = new DataLoaderContextAccessor();
var urlGenerator = new FakeUrlGenerator();
services = new Dictionary<Type, object>
{
[typeof(IAppProvider)] = appProvider,
@ -164,11 +167,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[typeof(ICommandBus)] = testBase.commandBus,
[typeof(IContentQueryService)] = testBase.contentQuery,
[typeof(IDataLoaderContextAccessor)] = dataLoaderContext,
[typeof(IOptions<AssetOptions>)] = Options.Create(new AssetOptions()),
[typeof(IOptions<ContentOptions>)] = Options.Create(new ContentOptions()),
[typeof(IUrlGenerator)] = urlGenerator,
[typeof(ISemanticLog)] = A.Fake<ISemanticLog>(),
[typeof(IUrlGenerator)] = new FakeUrlGenerator(),
[typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext)
[typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext),
[typeof(GraphQLTypeFactory)] = new GraphQLTypeFactory(urlGenerator)
};
}

Loading…
Cancel
Save