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. 266
      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. 8
      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. 121
      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 System.Threading.Tasks;
using GraphQL.Utilities; using GraphQL.Utilities;
using Microsoft.Extensions.Caching.Memory; 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.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Log; using Squidex.Log;
@ -41,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App); 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)); 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 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; 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)) if (string.IsNullOrWhiteSpace(query.Query))
{ {
return (false, new { data = new object() }); 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) if (errors?.Any() == true)
{ {
@ -93,23 +91,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return new GraphQLModel(app, return new GraphQLModel(app,
allSchemas, allSchemas,
GetPageSizeForContents(), resolver.GetRequiredService<GraphQLTypeFactory>(),
GetPageSizeForAssets(),
resolver.GetRequiredService<IUrlGenerator>(),
resolver.GetRequiredService<ISemanticLog>()); 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) private static object CreateCacheKey(DomainId appId, string etag)
{ {
return $"GraphQLModel_{appId}_{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 ICommandBus CommandBus { get; }
public ISemanticLog Log { get; }
public GraphQLExecutionContext(Context context, IServiceProvider resolver) public GraphQLExecutionContext(Context context, IServiceProvider resolver)
: base(context : base(context
.WithoutCleanup() .WithoutCleanup()
@ -44,6 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
CommandBus = resolver.GetRequiredService<ICommandBus>(); CommandBus = resolver.GetRequiredService<ICommandBus>();
Log = resolver.GetRequiredService<ISemanticLog>();
dataLoaderContextAccessor = resolver.GetRequiredService<IDataLoaderContextAccessor>(); dataLoaderContextAccessor = resolver.GetRequiredService<IDataLoaderContextAccessor>();
this.resolver = resolver; this.resolver = resolver;
@ -54,9 +58,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var loader = resolver.GetRequiredService<DataLoaderDocumentListener>(); var loader = resolver.GetRequiredService<DataLoaderDocumentListener>();
execution.Listeners.Add(loader); execution.Listeners.Add(loader);
execution.FieldMiddleware.Use(Middlewares.Logging(resolver.GetRequiredService<ISemanticLog>()));
execution.FieldMiddleware.Use(Middlewares.Errors());
execution.UserContext = this; 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.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; 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.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Log; using Squidex.Log;
@ -26,46 +27,48 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
private static readonly IDocumentExecuter Executor = new DocumentExecuter(); private static readonly IDocumentExecuter Executor = new DocumentExecuter();
private readonly Dictionary<DomainId, ContentGraphType> contentTypes = new Dictionary<DomainId, ContentGraphType>(); 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 GraphQLSchema graphQLSchema;
private readonly GraphQLTypeVisitor graphQLTypeVisitor; private readonly GraphQLTypeFactory graphQLTypeFactory;
private readonly ISemanticLog log; private readonly ISemanticLog log;
#pragma warning disable IDE0044 // Add readonly modifier
public bool CanGenerateAssetSourceUrl { get; } private GraphQLTypeVisitor typeVisitor;
#pragma warning disable IDE0044 // Add readonly modifier
private PartitionResolver partitionResolver;
#pragma warning restore IDE0044 // Add readonly modifier
static GraphQLModel() static GraphQLModel()
{ {
ValueConverter.Register<string, DomainId>(DomainId.Create); ValueConverter.Register<string, DomainId>(DomainId.Create);
} }
public GraphQLModel(IAppEntity app, public GraphQLTypeFactory TypeFactory
IEnumerable<ISchemaEntity> schemas,
int pageSizeContents,
int pageSizeAssets,
IUrlGenerator urlGenerator, ISemanticLog log)
{ {
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); partitionResolver = app.PartitionResolver();
assetListType = new ListGraphType(new NonNullGraphType(assetType));
graphQLTypeVisitor = new GraphQLTypeVisitor(contentTypes, this, assetListType); typeVisitor = new GraphQLTypeVisitor(contentTypes, this);
var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList();
BuildSchemas(allSchemas); BuildSchemas(allSchemas);
graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); graphQLSchema = BuildSchema(this, allSchemas);
graphQLSchema.RegisterValueConverter(JsonConverter.Instance); graphQLSchema.RegisterValueConverter(JsonConverter.Instance);
graphQLSchema.RegisterValueConverter(InstantConverter.Instance); graphQLSchema.RegisterValueConverter(InstantConverter.Instance);
InitializeContentTypes(allSchemas, pageSizeContents); InitializeContentTypes(allSchemas);
partitionResolver = null!;
typeVisitor = null!;
} }
private void BuildSchemas(List<ISchemaEntity> allSchemas) 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; var i = 0;
@ -84,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var schema = allSchemas[i]; var schema = allSchemas[i];
contentType.Initialize(this, schema, allSchemas, pageSize); contentType.Initialize(this, schema, allSchemas);
i++; i++;
} }
@ -93,19 +96,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
graphQLSchema.RegisterType(contentType); 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 var schema = new GraphQLSchema
{ {
Query = new AppQueriesGraphType( Query = new AppQueriesGraphType(model, schemas)
model,
pageSizeContents,
pageSizeAssets,
schemas)
}; };
schema.RegisterType(ContentInterfaceGraphType.Instance);
var schemasWithFields = schemas.Where(x => x.SchemaDef.Fields.Count > 0); var schemasWithFields = schemas.Where(x => x.SchemaDef.Fields.Count > 0);
if (schemasWithFields.Any()) 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) public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName)
{ {
return field.Accept(graphQLTypeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName)); return field.Accept(typeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName));
}
public IGraphType GetAssetType()
{
return assetType;
} }
public IGraphType GetContentType(DomainId schemaId) 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 public interface IGraphModel
{ {
bool CanGenerateAssetSourceUrl { get; }
IFieldPartitioning ResolvePartition(Partitioning key); IFieldPartitioning ResolvePartition(Partitioning key);
IGraphType GetAssetType(); GraphQLTypeFactory TypeFactory { get; }
IGraphType GetContentType(DomainId schemaId); 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 System;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets; 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 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 System.Collections.Generic;
using GraphQL.Types; 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.Contents.GraphQL.Types.Utils;
using Squidex.Domain.Apps.Entities.Schemas; 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 System.Collections.Generic;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -14,12 +15,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class AppQueriesGraphType : ObjectGraphType 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(); AddField(model.TypeFactory.FindAsset);
AddField(model.TypeFactory.QueryAssets);
AddAssetFind(assetType); AddField(model.TypeFactory.QueryAssetsWithTotal);
AddAssetsQueries(assetType, pageSizeAssets);
foreach (var schema in schemas) foreach (var schema in schemas)
{ {
@ -39,25 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
schemaId, schemaId,
schemaType, schemaType,
schemaName, schemaName,
contentType, contentType);
pageSizeContents);
} }
Description = "The app queries."; 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) private void AddContentFind(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType)
{ {
AddField(new FieldType AddField(new FieldType
@ -70,37 +57,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
}); });
} }
private void AddAssetsQueries(IGraphType assetType, int pageSize) private void AddContentQueries(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType)
{
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)
{ {
var resolver = ContentActions.QueryOrReferencing.Query(schemaId); var resolver = ContentActions.QueryOrReferencing.Query(schemaId);
AddField(new FieldType AddField(new FieldType
{ {
Name = $"query{schemaType}Contents", Name = $"query{schemaType}Contents",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = resolver, Resolver = resolver,
Description = $"Query {schemaName} content items." Description = $"Query {schemaName} content items."
@ -109,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = $"query{schemaType}ContentsWithTotal", Name = $"query{schemaType}ContentsWithTotal",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType), ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType),
Resolver = resolver, Resolver = resolver,
Description = $"Query {schemaName} content items with total count." 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.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"; Name = "Asset";
@ -68,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "mimeType", Name = "mimeType",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.MimeType, Resolver = Resolve(x => x.MimeType),
Description = "The mime type." Description = "The mime type."
}); });
@ -76,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "url", Name = "url",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.Url, Resolver = Url,
Description = "The url to the asset." Description = "The url to the asset."
}); });
@ -84,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "thumbnailUrl", Name = "thumbnailUrl",
ResolvedType = AllTypes.String, ResolvedType = AllTypes.String,
Resolver = AssetResolvers.ThumbnailUrl, Resolver = ThumbnailUrl,
Description = "The thumbnail url to the asset." Description = "The thumbnail url to the asset."
}); });
@ -92,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "fileName", Name = "fileName",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileName, Resolver = Resolve(x => x.FileName),
Description = "The file name." Description = "The file name."
}); });
@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "fileHash", Name = "fileHash",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileHash, Resolver = Resolve(x => x.FileHash),
Description = "The hash of the file. Can be null for old files." 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", Name = "fileType",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.FileType, Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type." Description = "The file type."
}); });
@ -116,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "fileSize", Name = "fileSize",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = AssetResolvers.FileSize, Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes." Description = "The size of the file in bytes."
}); });
@ -124,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "fileVersion", Name = "fileVersion",
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = AssetResolvers.FileVersion, Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file." Description = "The version of the file."
}); });
@ -132,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "slug", Name = "slug",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.Slug, Resolver = Resolve(x => x.Slug),
Description = "The file name as slug." Description = "The file name as slug."
}); });
@ -140,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "isProtected", Name = "isProtected",
ResolvedType = AllTypes.NonNullBoolean, ResolvedType = AllTypes.NonNullBoolean,
Resolver = AssetResolvers.IsProtected, Resolver = Resolve(x => x.IsProtected),
Description = "True, when the asset is not public." Description = "True, when the asset is not public."
}); });
@ -148,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "isImage", Name = "isImage",
ResolvedType = AllTypes.NonNullBoolean, ResolvedType = AllTypes.NonNullBoolean,
Resolver = AssetResolvers.IsImage, Resolver = Resolve(x => x.Type == AssetType.Image),
Description = "Determines if the uploaded file is an image.", Description = "Determines if the uploaded file is an image.",
DeprecationReason = "Use 'type' field instead." DeprecationReason = "Use 'type' field instead."
}); });
@ -157,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "pixelWidth", Name = "pixelWidth",
ResolvedType = AllTypes.Int, 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.", Description = "The width of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead." DeprecationReason = "Use 'metadata' field instead."
}); });
@ -166,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "pixelHeight", Name = "pixelHeight",
ResolvedType = AllTypes.Int, 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.", Description = "The height of the image in pixels if the asset is an image.",
DeprecationReason = "Use 'metadata' field instead." DeprecationReason = "Use 'metadata' field instead."
}); });
@ -175,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "type", Name = "type",
ResolvedType = AllTypes.NonNullAssetType, ResolvedType = AllTypes.NonNullAssetType,
Resolver = AssetResolvers.Type, Resolver = Resolve(x => x.Type),
Description = "The type of the image." Description = "The type of the image."
}); });
@ -183,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "metadataText", Name = "metadataText",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.MetadataText, Resolver = Resolve(x => x.MetadataText),
Description = "The text representation of the metadata." Description = "The text representation of the metadata."
}); });
@ -191,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "tags", Name = "tags",
ResolvedType = null, ResolvedType = null,
Resolver = AssetResolvers.Tags, Resolver = Resolve(x => x.TagNames),
Description = "The asset tags.", Description = "The asset tags.",
Type = AllTypes.NonNullTagsType Type = AllTypes.NonNullTagsType
}); });
@ -205,18 +211,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "The asset metadata." Description = "The asset metadata."
}); });
if (model.CanGenerateAssetSourceUrl) if (canGenerateSourceUrl)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = "sourceUrl", Name = "sourceUrl",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = AssetResolvers.SourceUrl, Resolver = SourceUrl,
Description = "The source url of the asset." Description = "The source url of the asset."
}); });
} }
Description = "An 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure; 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"; Name = "AssetResultDto";
AddField(new FieldType AddField(new FieldType
{ {
Name = "total", Name = "total",
ResolvedType = AllTypes.Int, ResolvedType = AllTypes.NonNullInt,
Resolver = AssetResolvers.ListTotal, Resolver = ResolveList(x => x.Total),
Description = "The total count of assets." Description = "The total count of assets."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "items", Name = "items",
Resolver = AssetResolvers.ListItems, ResolvedType = assetsList,
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), Resolver = ResolveList(x => x),
Description = "The assets." Description = "The assets."
}); });
Description = "List of assets and total count of 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.";
}
}
}

266
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 NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands; 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;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Validation; 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 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 class Json
{ {
public static readonly QueryArguments Arguments = new QueryArguments 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); 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 public static readonly QueryArguments Arguments = new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{
Name = "id",
Description = "The id of the content (usually GUID).",
DefaultValue = null,
ResolvedType = AllTypes.NonNullDomainId
},
new QueryArgument(AllTypes.None) new QueryArgument(AllTypes.None)
{ {
Name = "version", Name = "version",
@ -85,19 +111,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
var schemaIdValue = schemaId.ToString(); 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) if (version >= 0)
{ {
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(schemaIdValue, contentId, version.Value); return await context.FindContentAsync(schemaIdValue, contentId, version.Value);
} }
else else
{ {
return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId); return await context.FindContentAsync(contentId);
} }
}); });
} }
@ -105,59 +131,54 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static class QueryOrReferencing public static class QueryOrReferencing
{ {
private static QueryArguments? arguments; public static readonly QueryArguments Arguments = new QueryArguments
public static QueryArguments Arguments(int pageSize)
{ {
return arguments ??= new QueryArguments new QueryArgument(AllTypes.None)
{ {
new QueryArgument(AllTypes.None) Name = "top",
{ Description = $"Optional number of contents to take.",
Name = "top", DefaultValue = null,
Description = $"Optional number of contents to take (Default: {pageSize}).", ResolvedType = AllTypes.Int
DefaultValue = pageSize, },
ResolvedType = AllTypes.Int new QueryArgument(AllTypes.None)
}, {
new QueryArgument(AllTypes.None) Name = "skip",
{ Description = "Optional number of contents to skip.",
Name = "skip", DefaultValue = 0,
Description = "Optional number of contents to skip.", ResolvedType = AllTypes.Int
DefaultValue = 0, },
ResolvedType = AllTypes.Int new QueryArgument(AllTypes.None)
}, {
new QueryArgument(AllTypes.None) Name = "filter",
{ Description = "Optional OData filter.",
Name = "filter", DefaultValue = null,
Description = "Optional OData filter.", ResolvedType = AllTypes.String
DefaultValue = null, },
ResolvedType = AllTypes.String new QueryArgument(AllTypes.None)
}, {
new QueryArgument(AllTypes.None) Name = "orderby",
{ Description = "Optional OData order definition.",
Name = "orderby", DefaultValue = null,
Description = "Optional OData order definition.", ResolvedType = AllTypes.String
DefaultValue = null, },
ResolvedType = AllTypes.String new QueryArgument(AllTypes.None)
}, {
new QueryArgument(AllTypes.None) Name = "search",
{ Description = "Optional OData full text search.",
Name = "search", DefaultValue = null,
Description = "Optional OData full text search.", ResolvedType = AllTypes.String
DefaultValue = null, }
ResolvedType = AllTypes.String };
}
};
}
public static IFieldResolver Query(DomainId schemaId) public static IFieldResolver Query(DomainId schemaId)
{ {
var schemaIdValue = schemaId.ToString(); 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(); 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, DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType) ResolvedType = new NonNullGraphType(inputType)
}, },
new QueryArgument(AllTypes.None) Publish, NewId
{
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
}
}; };
} }
@ -234,13 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
return new QueryArguments return new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = null,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None) new QueryArgument(AllTypes.None)
{ {
Name = "data", Name = "data",
@ -248,20 +250,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null, DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType) ResolvedType = new NonNullGraphType(inputType)
}, },
new QueryArgument(AllTypes.None) Publish,
{ ExpectedVersion
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
}
}; };
} }
@ -286,13 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
return new QueryArguments return new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None) new QueryArgument(AllTypes.None)
{ {
Name = "data", Name = "data",
@ -300,13 +284,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null, DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType) ResolvedType = new NonNullGraphType(inputType)
}, },
new QueryArgument(AllTypes.None) ExpectedVersion
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
}; };
} }
@ -328,13 +306,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
return new QueryArguments return new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None) new QueryArgument(AllTypes.None)
{ {
Name = "data", Name = "data",
@ -342,13 +314,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null, DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType) ResolvedType = new NonNullGraphType(inputType)
}, },
new QueryArgument(AllTypes.None) ExpectedVersion
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
}; };
} }
@ -368,13 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public static readonly QueryArguments Arguments = new QueryArguments public static readonly QueryArguments Arguments = new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{
Name = "id",
Description = "The id of the content (usually GUID)",
DefaultValue = null,
ResolvedType = AllTypes.NonNullString
},
new QueryArgument(AllTypes.None) new QueryArgument(AllTypes.None)
{ {
Name = "status", Name = "status",
@ -389,13 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
DefaultValue = null, DefaultValue = null,
ResolvedType = AllTypes.Date ResolvedType = AllTypes.Date
}, },
new QueryArgument(AllTypes.None) ExpectedVersion
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
}; };
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId) 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 public static readonly QueryArguments Arguments = new QueryArguments
{ {
new QueryArgument(AllTypes.None) Id,
{ ExpectedVersion
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
}
}; };
public static IFieldResolver Resolver(NamedId<DomainId> appId, NamedId<DomainId> schemaId) 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) 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 try
{ {
var command = action(c); var command = action(fieldContext);
command.AppId = appId; command.AppId = appId;
command.SchemaId = schemaId; 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>(); return commandContext.Result<T>();
} }
catch (ValidationException ex) catch (ValidationException ex)
{ {
c.Errors.Add(new ExecutionError(ex.Message)); fieldContext.Errors.Add(new ExecutionError(ex.Message));
throw; throw;
} }
catch (DomainException ex) catch (DomainException ex)
{ {
c.Errors.Add(new ExecutionError(ex.Message)); fieldContext.Errors.Add(new ExecutionError(ex.Message));
throw; 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.Core.Contents;
using Squidex.Domain.Apps.Entities.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 ContentDataFlatGraphType : ObjectGraphType<FlatContentData> public sealed class ContentDataFlatGraphType : ObjectGraphType<FlatContentData>
{ {
@ -27,8 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = fieldName, Name = fieldName,
Arguments = args, Arguments = args,
Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name),
ResolvedType = resolvedType, ResolvedType = resolvedType,
Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name),
Description = field.RawProperties.Hints 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.Core.Schemas;
using Squidex.Domain.Apps.Entities.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> public sealed class ContentDataGraphType : ObjectGraphType<ContentData>
{ {
@ -39,8 +39,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = partitionKey.EscapePartition(), Name = partitionKey.EscapePartition(),
Arguments = args, Arguments = args,
Resolver = ContentResolvers.Partition(valueResolver, partitionKey),
ResolvedType = resolvedType, ResolvedType = resolvedType,
Resolver = ContentResolvers.Partition(valueResolver, partitionKey),
Description = field.RawProperties.Hints Description = field.RawProperties.Hints
}); });
} }
@ -50,8 +50,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = fieldName, Name = fieldName,
Resolver = ContentResolvers.Field(field),
ResolvedType = fieldGraphType, ResolvedType = fieldGraphType,
Resolver = ContentResolvers.Field(field),
Description = $"The {displayName} field." Description = $"The {displayName} field."
}); });
} }

8
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.Core.Schemas;
using Squidex.Domain.Apps.Entities.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 public sealed class ContentDataInputGraphType : InputObjectGraphType
{ {
@ -38,10 +38,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
fieldGraphType.AddField(new FieldType fieldGraphType.AddField(new FieldType
{ {
Name = partitionKey.EscapePartition(), Name = partitionKey.EscapePartition(),
Resolver = null,
ResolvedType = resolvedType, ResolvedType = resolvedType,
Resolver = null,
Description = field.RawProperties.Hints Description = field.RawProperties.Hints
}).WithSourceName( partitionKey); }).WithSourceName(partitionKey);
} }
fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content input type."; fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content input type.";
@ -49,8 +49,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = fieldName, Name = fieldName,
Resolver = null,
ResolvedType = fieldGraphType, ResolvedType = fieldGraphType,
Resolver = null,
Description = $"The {displayName} field." Description = $"The {displayName} field."
}).WithSourceName(field.Name); }).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.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; 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> public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{ {
@ -27,71 +27,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = schemaType.SafeTypeName(); Name = schemaType.SafeTypeName();
AddField(new FieldType AddField(ContentFields.Id);
{ AddField(ContentFields.Version);
Name = "id", AddField(ContentFields.Created);
ResolvedType = AllTypes.NonNullString, AddField(ContentFields.CreatedBy);
Resolver = EntityResolvers.Id, AddField(ContentFields.LastModified);
Description = $"The id of the {schemaName} content." AddField(ContentFields.LastModifiedBy);
}); AddField(ContentFields.Status);
AddField(ContentFields.StatusColor);
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."
});
Interface<ContentInterfaceGraphType>(); AddResolvedInterface(ContentInterfaceGraphType.Instance);
Description = $"The structure of a {schemaName} content type."; 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; 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 schemaType = schema.TypeName();
var schemaName = schema.DisplayName(); var schemaName = schema.DisplayName();
@ -113,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "url", Name = "url",
ResolvedType = AllTypes.NonNullString, ResolvedType = AllTypes.NonNullString,
Resolver = ContentResolvers.Url, 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); var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model);
@ -125,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "data", Name = "data",
ResolvedType = new NonNullGraphType(contentDataType), ResolvedType = new NonNullGraphType(contentDataType),
Resolver = ContentResolvers.Data, 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", Name = "flatData",
ResolvedType = new NonNullGraphType(contentDataTypeFlat), ResolvedType = new NonNullGraphType(contentDataTypeFlat),
Resolver = ContentResolvers.FlatData, 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); 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); var resolver = ContentActions.QueryOrReferencing.Referencing(referencingId);
AddField(new FieldType AddField(new FieldType
{ {
Name = $"referencing{referencingType}Contents", Name = $"referencing{referencingType}Contents",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = resolver, Resolver = resolver,
Description = $"Query {referencingName} content items." Description = $"Query {referencingName} content items."
@ -170,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = $"referencing{referencingType}ContentsWithTotal", Name = $"referencing{referencingType}ContentsWithTotal",
Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), Arguments = ContentActions.QueryOrReferencing.Arguments,
ResolvedType = new ContentsResultGraphType(referencingType, referencingName, contentType), ResolvedType = new ContentsResultGraphType(referencingType, referencingName, contentType),
Resolver = resolver, Resolver = resolver,
Description = $"Query {referencingName} content items with total count." 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;
using Squidex.Infrastructure.Json.Objects; 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) 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; return null;
@ -34,11 +34,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static IFieldResolver Partition(ValueResolver valueResolver, string key) 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; return null;
@ -47,11 +47,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static IFieldResolver FlatPartition(ValueResolver valueResolver, string key) 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; return null;
@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
var fieldName = field.Name; 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 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 ListTotal = ResolveList(x => x.Total);
public static readonly IFieldResolver ListItems = ResolveList(x => x); 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 GraphQL.Types;
using Squidex.Infrastructure; 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 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 GraphQL.Types;
using Squidex.Infrastructure; 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>> public sealed class ContentsResultGraphType : ObjectGraphType<IResultList<IContentEntity>>
{ {
@ -19,16 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType AddField(new FieldType
{ {
Name = "total", Name = "total",
Resolver = ContentResolvers.ListTotal,
ResolvedType = AllTypes.NonNullInt, ResolvedType = AllTypes.NonNullInt,
Resolver = ContentResolvers.ListTotal,
Description = $"The total number of {schemaName} items." Description = $"The total number of {schemaName} items."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "items", Name = "items",
Resolver = ContentResolvers.ListItems,
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = ContentResolvers.ListItems,
Description = $"The {schemaName} items." Description = $"The {schemaName} items."
}); });

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

@ -10,9 +10,12 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using GraphQL; using GraphQL;
using GraphQL.Types; using GraphQL.Types;
using GraphQL.Utilities;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.ObjectPool;
using Squidex.Text; 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 #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) public static string BuildODataQuery(this IResolveFieldContext context)
{ {
var odataQuery = "?" + var sb = DefaultPools.StringBuilder.Get();
string.Join("&", try
context.Arguments {
.Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) sb.Append('?');
.Select(x => $"${x.Key}={x.Value}"));
foreach (var argument in context.Arguments)
return odataQuery; {
var value = argument.Value?.ToString();
if (!string.IsNullOrWhiteSpace(value))
{
if (sb.Length > 1)
{
sb.Append('&');
}
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) 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; return field;
} }
public static string GetSourceName(this FieldType 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) public static IGraphType Flatten(this QueryArgument type)
@ -98,5 +129,77 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return type; 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;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types 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> 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 Dictionary<DomainId, ContentGraphType> schemaTypes;
private readonly IGraphType assetListType;
private readonly IGraphModel model; private readonly IGraphModel model;
public readonly struct Args public readonly struct Args
@ -38,13 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
} }
} }
public GraphQLTypeVisitor( public GraphQLTypeVisitor(Dictionary<DomainId, ContentGraphType> schemaTypes, IGraphModel model)
Dictionary<DomainId, ContentGraphType> schemaTypes,
IGraphModel model,
IGraphType assetListType)
{ {
this.assetListType = assetListType;
this.model = model; this.model = model;
this.schemaTypes = schemaTypes; this.schemaTypes = schemaTypes;
} }
@ -115,14 +113,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets() 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 context.GetReferencedAssetsAsync(value);
}); });
return (assetListType, resolver, null); return (model.TypeFactory.AssetsList, resolver, null);
} }
private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField<ReferencesFieldProperties> field, Args args) private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField<ReferencesFieldProperties> field, Args args)
@ -141,10 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
contentType = union; contentType = union;
} }
var resolver = new ValueResolver((value, c) => var resolver = new ValueResolver((value, _, context) =>
{ {
var context = (GraphQLExecutionContext)c.UserContext;
return context.GetReferencedContentsAsync(value); return context.GetReferencedContentsAsync(value);
}); });

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

@ -8,6 +8,7 @@
using System.Linq; using System.Linq;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
@ -30,14 +31,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
if (resolvedType != null && valueResolver != null) if (resolvedType != null && valueResolver != null)
{ {
var resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name);
AddField(new FieldType AddField(new FieldType
{ {
Name = nestedName, Name = nestedName,
Arguments = args, Arguments = args,
ResolvedType = resolvedType, ResolvedType = resolvedType,
Resolver = resolver, Resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name),
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." 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 AddField(new FieldType
{ {
Name = nestedName, Name = nestedName,
Resolver = null,
ResolvedType = resolvedType, ResolvedType = resolvedType,
Resolver = null,
Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field."
}).WithSourceName(nestedField.Name); }).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.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects; 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) 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 System;
using GraphQL.Resolvers; 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 Id = Resolve<IEntity>(x => x.Id.ToString());
public static readonly IFieldResolver Created = Resolve<IEntity>(x => x.Created); 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 LastModifiedBy = Resolve<IEntityWithLastModifiedBy>(x => x.LastModifiedBy.ToString());
public static readonly IFieldResolver Version = Resolve<IEntityWithVersion>(x => x.Version); 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 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(); public static readonly IGraphType Nullable = new EntitySavedGraphType();
@ -34,10 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils
private static IFieldResolver ResolveVersion() private static IFieldResolver ResolveVersion()
{ {
return new FuncFieldResolver<EntitySavedResult, long>(x => return Resolvers.Sync<EntitySavedResult, long>(x => x.Version);
{
return x.Source.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 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(); 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 GraphQL.Types;
using NodaTime; 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(); 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 GraphQL.Types;
using NodaTime.Text; 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) 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 GraphQL.Language.AST;
using NodaTime; 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) 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 GraphQL.Types;
using Squidex.Infrastructure.Json.Objects; 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(); 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.Language.AST;
using GraphQL.Types; 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() 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 GraphQL.Language.AST;
using Squidex.Infrastructure.Json.Objects; 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) 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.Language.AST;
using GraphQL.Types; 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) 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 Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Web.Services; using Squidex.Web.Services;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
@ -29,8 +30,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<DataLoaderDocumentListener>() services.AddSingletonAs<DataLoaderDocumentListener>()
.AsSelf(); .AsSelf();
services.AddSingletonAs<CachingGraphQLService>() services.AddSingletonAs<GraphQLTypeFactory>()
.As<IGraphQLService>(); .AsSelf();
services.AddSingletonAs<CachingGraphQLService>() services.AddSingletonAs<CachingGraphQLService>()
.As<IGraphQLService>(); .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.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; 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.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -157,6 +158,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var dataLoaderContext = new DataLoaderContextAccessor(); var dataLoaderContext = new DataLoaderContextAccessor();
var urlGenerator = new FakeUrlGenerator();
services = new Dictionary<Type, object> services = new Dictionary<Type, object>
{ {
[typeof(IAppProvider)] = appProvider, [typeof(IAppProvider)] = appProvider,
@ -164,11 +167,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[typeof(ICommandBus)] = testBase.commandBus, [typeof(ICommandBus)] = testBase.commandBus,
[typeof(IContentQueryService)] = testBase.contentQuery, [typeof(IContentQueryService)] = testBase.contentQuery,
[typeof(IDataLoaderContextAccessor)] = dataLoaderContext, [typeof(IDataLoaderContextAccessor)] = dataLoaderContext,
[typeof(IOptions<AssetOptions>)] = Options.Create(new AssetOptions()), [typeof(IUrlGenerator)] = urlGenerator,
[typeof(IOptions<ContentOptions>)] = Options.Create(new ContentOptions()),
[typeof(ISemanticLog)] = A.Fake<ISemanticLog>(), [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