From 14da310d148321db0af24caa44091236d5f4a0cc Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 26 Jan 2021 21:51:47 +0100 Subject: [PATCH] Refactoring/reduce memory usage (#629) * Reduce memory usage for graphql. * Added missing files. * More allocation improvements. * Removed backup file. --- .../Contents/GraphQL/CachingGraphQLService.cs | 30 +- .../GraphQL/GraphQLExecutionContext.cs | 7 +- .../Contents/GraphQL/GraphQLModel.cs | 67 +++-- .../Contents/GraphQL/IGraphModel.cs | 4 +- .../Contents/GraphQL/Middlewares.cs | 62 ---- .../Contents/GraphQL/Types/AllTypes.cs | 2 +- .../GraphQL/Types/AppMutationsGraphType.cs | 1 + .../GraphQL/Types/AppQueriesGraphType.cs | 54 +--- .../Contents/GraphQL/Types/AssetActions.cs | 112 -------- .../Contents/GraphQL/Types/AssetResolvers.cs | 66 ----- .../GraphQL/Types/Assets/AssetActions.cs | 107 +++++++ .../Types/{ => Assets}/AssetGraphType.cs | 73 +++-- .../{ => Assets}/AssetsResultGraphType.cs | 21 +- .../Types/ContentInterfaceGraphType.cs | 85 ------ .../Types/{ => Contents}/ContentActions.cs | 266 +++++++----------- .../ContentDataFlatGraphType.cs | 4 +- .../{ => Contents}/ContentDataGraphType.cs | 6 +- .../ContentDataInputGraphType.cs | 8 +- .../GraphQL/Types/Contents/ContentFields.cs | 86 ++++++ .../Types/{ => Contents}/ContentGraphType.cs | 91 ++---- .../Contents/ContentInterfaceGraphType.cs | 32 +++ .../Types/{ => Contents}/ContentResolvers.cs | 42 +-- .../{ => Contents}/ContentUnionGraphType.cs | 2 +- .../{ => Contents}/ContentsResultGraphType.cs | 6 +- .../Contents/GraphQL/Types/Extensions.cs | 121 +++++++- .../GraphQL/Types/GraphQLTypeFactory.cs | 90 ++++++ .../GraphQL/Types/GraphQLTypeVisitor.cs | 24 +- .../Contents/GraphQL/Types/NestedGraphType.cs | 5 +- .../GraphQL/Types/NestedInputGraphType.cs | 2 +- .../Types/{Utils => Primitives}/Converters.cs | 4 +- .../Types/{ => Primitives}/EntityResolvers.cs | 8 +- .../EntitySavedGraphType.cs | 7 +- .../GeolocationInputGraphType.cs | 2 +- .../{Utils => Primitives}/InstantConverter.cs | 4 +- .../{Utils => Primitives}/InstantGraphType.cs | 4 +- .../{Utils => Primitives}/InstantValueNode.cs | 4 +- .../{Utils => Primitives}/JsonConverter.cs | 4 +- .../{Utils => Primitives}/JsonGraphType.cs | 4 +- .../{Utils => Primitives}/JsonValueNode.cs | 4 +- .../{Utils => Primitives}/NoopGraphType.cs | 4 +- .../Contents/GraphQL/Types/Resolvers.cs | 124 ++++++++ .../Squidex/Config/Domain/QueryServices.cs | 5 +- .../Contents/GraphQL/GraphQLTestBase.cs | 10 +- 43 files changed, 873 insertions(+), 791 deletions(-) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Assets}/AssetGraphType.cs (73%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Assets}/AssetsResultGraphType.cs (59%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentActions.cs (60%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentDataFlatGraphType.cs (95%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentDataGraphType.cs (97%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentDataInputGraphType.cs (94%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentGraphType.cs (62%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentResolvers.cs (62%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentUnionGraphType.cs (96%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Contents}/ContentsResultGraphType.cs (94%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeFactory.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/Converters.cs (96%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{ => Primitives}/EntityResolvers.cs (84%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/EntitySavedGraphType.cs (84%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/GeolocationInputGraphType.cs (93%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/InstantConverter.cs (85%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/InstantGraphType.cs (89%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/InstantValueNode.cs (82%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/JsonConverter.cs (93%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/JsonGraphType.cs (88%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/JsonValueNode.cs (83%) rename backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/{Utils => Primitives}/NoopGraphType.cs (88%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs index 9d7467c86..9e09e9bd4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -10,10 +10,8 @@ using System.Linq; using System.Threading.Tasks; using GraphQL.Utilities; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Infrastructure; using Squidex.Log; @@ -41,9 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, resolver); + var graphQlContext = new GraphQLExecutionContext(context, resolver); - var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q))); + var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, graphQlContext, q))); return (result.Any(x => x.HasError), result.Map(x => x.Response)); } @@ -55,21 +53,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var model = await GetModelAsync(context.App); - var ctx = new GraphQLExecutionContext(context, resolver); + var graphQlContext = new GraphQLExecutionContext(context, resolver); - var result = await QueryInternalAsync(model, ctx, query); + var result = await QueryInternalAsync(model, graphQlContext, query); return result; } - private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext ctx, GraphQLQuery query) + private static async Task<(bool HasError, object Response)> QueryInternalAsync(GraphQLModel model, GraphQLExecutionContext context, GraphQLQuery query) { if (string.IsNullOrWhiteSpace(query.Query)) { return (false, new { data = new object() }); } - var (data, errors) = await model.ExecuteAsync(ctx, query); + var (data, errors) = await model.ExecuteAsync(context, query); if (errors?.Any() == true) { @@ -93,23 +91,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return new GraphQLModel(app, allSchemas, - GetPageSizeForContents(), - GetPageSizeForAssets(), - resolver.GetRequiredService(), + resolver.GetRequiredService(), resolver.GetRequiredService()); }); } - private int GetPageSizeForContents() - { - return resolver.GetRequiredService>().Value.DefaultPageSizeGraphQl; - } - - private int GetPageSizeForAssets() - { - return resolver.GetRequiredService>().Value.DefaultPageSizeGraphQl; - } - private static object CreateCacheKey(DomainId appId, string etag) { return $"GraphQLModel_{appId}_{etag}"; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index 643b6a14a..038da926f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -33,6 +33,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public ICommandBus CommandBus { get; } + public ISemanticLog Log { get; } + public GraphQLExecutionContext(Context context, IServiceProvider resolver) : base(context .WithoutCleanup() @@ -44,6 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL CommandBus = resolver.GetRequiredService(); + Log = resolver.GetRequiredService(); + dataLoaderContextAccessor = resolver.GetRequiredService(); this.resolver = resolver; @@ -54,9 +58,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var loader = resolver.GetRequiredService(); execution.Listeners.Add(loader); - execution.FieldMiddleware.Use(Middlewares.Logging(resolver.GetRequiredService())); - execution.FieldMiddleware.Use(Middlewares.Errors()); - execution.UserContext = this; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs index c0b2c06ac..e28fc4a5f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -14,7 +14,8 @@ using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Log; @@ -26,46 +27,48 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { private static readonly IDocumentExecuter Executor = new DocumentExecuter(); private readonly Dictionary contentTypes = new Dictionary(); - private readonly PartitionResolver partitionResolver; - private readonly IObjectGraphType assetType; - private readonly IGraphType assetListType; private readonly GraphQLSchema graphQLSchema; - private readonly GraphQLTypeVisitor graphQLTypeVisitor; + private readonly GraphQLTypeFactory graphQLTypeFactory; private readonly ISemanticLog log; - - public bool CanGenerateAssetSourceUrl { get; } +#pragma warning disable IDE0044 // Add readonly modifier + private GraphQLTypeVisitor typeVisitor; +#pragma warning disable IDE0044 // Add readonly modifier + private PartitionResolver partitionResolver; +#pragma warning restore IDE0044 // Add readonly modifier static GraphQLModel() { ValueConverter.Register(DomainId.Create); } - public GraphQLModel(IAppEntity app, - IEnumerable schemas, - int pageSizeContents, - int pageSizeAssets, - IUrlGenerator urlGenerator, ISemanticLog log) + public GraphQLTypeFactory TypeFactory { - this.log = log; + get { return graphQLTypeFactory; } + } - partitionResolver = app.PartitionResolver(); + public GraphQLModel(IAppEntity app, IEnumerable schemas, GraphQLTypeFactory typeFactory, ISemanticLog log) + { + graphQLTypeFactory = typeFactory; - CanGenerateAssetSourceUrl = urlGenerator.CanGenerateAssetSourceUrl; + this.log = log; - assetType = new AssetGraphType(this); - assetListType = new ListGraphType(new NonNullGraphType(assetType)); + partitionResolver = app.PartitionResolver(); - graphQLTypeVisitor = new GraphQLTypeVisitor(contentTypes, this, assetListType); + typeVisitor = new GraphQLTypeVisitor(contentTypes, this); var allSchemas = schemas.Where(x => x.SchemaDef.IsPublished).ToList(); BuildSchemas(allSchemas); - graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); + graphQLSchema = BuildSchema(this, allSchemas); graphQLSchema.RegisterValueConverter(JsonConverter.Instance); graphQLSchema.RegisterValueConverter(InstantConverter.Instance); - InitializeContentTypes(allSchemas, pageSizeContents); + InitializeContentTypes(allSchemas); + + partitionResolver = null!; + + typeVisitor = null!; } private void BuildSchemas(List allSchemas) @@ -76,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL } } - private void InitializeContentTypes(List allSchemas, int pageSize) + private void InitializeContentTypes(List allSchemas) { var i = 0; @@ -84,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { var schema = allSchemas[i]; - contentType.Initialize(this, schema, allSchemas, pageSize); + contentType.Initialize(this, schema, allSchemas); i++; } @@ -93,19 +96,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { graphQLSchema.RegisterType(contentType); } + + graphQLSchema.Initialize(); + graphQLSchema.CleanupMetadata(); } - private static GraphQLSchema BuildSchema(GraphQLModel model, int pageSizeContents, int pageSizeAssets, List schemas) + private static GraphQLSchema BuildSchema(GraphQLModel model, List schemas) { var schema = new GraphQLSchema { - Query = new AppQueriesGraphType( - model, - pageSizeContents, - pageSizeAssets, - schemas) + Query = new AppQueriesGraphType(model, schemas) }; + schema.RegisterType(ContentInterfaceGraphType.Instance); + var schemasWithFields = schemas.Where(x => x.SchemaDef.Fields.Count > 0); if (schemasWithFields.Any()) @@ -128,12 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName) { - return field.Accept(graphQLTypeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName)); - } - - public IGraphType GetAssetType() - { - return assetType; + return field.Accept(typeVisitor, new GraphQLTypeVisitor.Args(schema, fieldName)); } public IGraphType GetContentType(DomainId schemaId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs index d8f7fda3b..73e978fe0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs @@ -16,11 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public interface IGraphModel { - bool CanGenerateAssetSourceUrl { get; } - IFieldPartitioning ResolvePartition(Partitioning key); - IGraphType GetAssetType(); + GraphQLTypeFactory TypeFactory { get; } IGraphType GetContentType(DomainId schemaId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs deleted file mode 100644 index e3d5d5125..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs +++ /dev/null @@ -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 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 Errors() - { - return (_, next) => - { - return async context => - { - try - { - return await next(context); - } - catch (DomainException ex) - { - throw new ExecutionError(ex.Message); - } - }; - }; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs index 45b126b89..8c3c8a5a1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs @@ -8,7 +8,7 @@ using System; using GraphQL.Types; using Squidex.Domain.Apps.Core.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs index dac09a52b..5d9ec566e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; using Squidex.Domain.Apps.Entities.Schemas; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs index d65f0e4e0..cf8ed66d6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -14,12 +15,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public sealed class AppQueriesGraphType : ObjectGraphType { - public AppQueriesGraphType(IGraphModel model, int pageSizeContents, int pageSizeAssets, IEnumerable schemas) + public AppQueriesGraphType(IGraphModel model, IEnumerable schemas) { - var assetType = model.GetAssetType(); - - AddAssetFind(assetType); - AddAssetsQueries(assetType, pageSizeAssets); + AddField(model.TypeFactory.FindAsset); + AddField(model.TypeFactory.QueryAssets); + AddField(model.TypeFactory.QueryAssetsWithTotal); foreach (var schema in schemas) { @@ -39,25 +39,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types schemaId, schemaType, schemaName, - contentType, - pageSizeContents); + contentType); } Description = "The app queries."; } - private void AddAssetFind(IGraphType assetType) - { - AddField(new FieldType - { - Name = "findAsset", - Arguments = AssetActions.Find.Arguments, - ResolvedType = assetType, - Resolver = AssetActions.Find.Resolver, - Description = "Find an asset by id." - }); - } - private void AddContentFind(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType) { AddField(new FieldType @@ -70,37 +57,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); } - private void AddAssetsQueries(IGraphType assetType, int pageSize) - { - var resolver = AssetActions.Query.Resolver; - - AddField(new FieldType - { - Name = "queryAssets", - Arguments = AssetActions.Query.Arguments(pageSize), - ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), - Resolver = resolver, - Description = "Get assets." - }); - - AddField(new FieldType - { - Name = "queryAssetsWithTotal", - Arguments = AssetActions.Query.Arguments(pageSize), - ResolvedType = new AssetsResultGraphType(assetType), - Resolver = resolver, - Description = "Get assets and total count." - }); - } - - private void AddContentQueries(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType, int pageSize) + private void AddContentQueries(DomainId schemaId, string schemaType, string schemaName, IGraphType contentType) { var resolver = ContentActions.QueryOrReferencing.Query(schemaId); AddField(new FieldType { Name = $"query{schemaType}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), Resolver = resolver, Description = $"Query {schemaName} content items." @@ -109,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = $"query{schemaType}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType), Resolver = resolver, Description = $"Query {schemaName} content items with total count." diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs deleted file mode 100644 index 9cf14ff05..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs +++ /dev/null @@ -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(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(c => - { - var assetId = c.GetArgument("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(c => - { - var query = c.BuildODataQuery(); - - return ((GraphQLExecutionContext)c.UserContext).QueryAssetsAsync(query); - }); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs deleted file mode 100644 index 4b101b786..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs +++ /dev/null @@ -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(Func action) - { - return new FuncFieldResolver(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext)); - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - - private static IFieldResolver ResolveList(Func, T> action) - { - return new FuncFieldResolver, object?>(c => action(c.Source)); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetActions.cs new file mode 100644 index 000000000..e0174eff8 --- /dev/null +++ b/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((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(async (_, fieldContext, context) => + { + var assetId = fieldContext.GetArgument("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(async (_, fieldContext, context) => + { + var query = fieldContext.BuildODataQuery(); + + return await context.QueryAssetsAsync(query); + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs similarity index 73% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs index ceb5b58e9..ba2b08185 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs @@ -5,14 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using GraphQL; +using GraphQL.Resolvers; using GraphQL.Types; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; +using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets { - public sealed class AssetGraphType : ObjectGraphType + internal sealed class AssetGraphType : ObjectGraphType { - public AssetGraphType(IGraphModel model) + public AssetGraphType(bool canGenerateSourceUrl) { Name = "Asset"; @@ -68,7 +74,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "mimeType", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.MimeType, + Resolver = Resolve(x => x.MimeType), Description = "The mime type." }); @@ -76,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "url", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.Url, + Resolver = Url, Description = "The url to the asset." }); @@ -84,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "thumbnailUrl", ResolvedType = AllTypes.String, - Resolver = AssetResolvers.ThumbnailUrl, + Resolver = ThumbnailUrl, Description = "The thumbnail url to the asset." }); @@ -92,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileName", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.FileName, + Resolver = Resolve(x => x.FileName), Description = "The file name." }); @@ -100,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileHash", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.FileHash, + Resolver = Resolve(x => x.FileHash), Description = "The hash of the file. Can be null for old files." }); @@ -108,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileType", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.FileType, + Resolver = Resolve(x => x.FileName.FileType()), Description = "The file type." }); @@ -116,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileSize", ResolvedType = AllTypes.NonNullInt, - Resolver = AssetResolvers.FileSize, + Resolver = Resolve(x => x.FileSize), Description = "The size of the file in bytes." }); @@ -124,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileVersion", ResolvedType = AllTypes.NonNullInt, - Resolver = AssetResolvers.FileVersion, + Resolver = Resolve(x => x.FileVersion), Description = "The version of the file." }); @@ -132,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "slug", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.Slug, + Resolver = Resolve(x => x.Slug), Description = "The file name as slug." }); @@ -140,7 +146,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "isProtected", ResolvedType = AllTypes.NonNullBoolean, - Resolver = AssetResolvers.IsProtected, + Resolver = Resolve(x => x.IsProtected), Description = "True, when the asset is not public." }); @@ -148,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "isImage", ResolvedType = AllTypes.NonNullBoolean, - Resolver = AssetResolvers.IsImage, + Resolver = Resolve(x => x.Type == AssetType.Image), Description = "Determines if the uploaded file is an image.", DeprecationReason = "Use 'type' field instead." }); @@ -157,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "pixelWidth", ResolvedType = AllTypes.Int, - Resolver = AssetResolvers.PixelWidth, + Resolver = Resolve(x => x.Metadata.GetPixelWidth()), Description = "The width of the image in pixels if the asset is an image.", DeprecationReason = "Use 'metadata' field instead." }); @@ -166,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "pixelHeight", ResolvedType = AllTypes.Int, - Resolver = AssetResolvers.PixelHeight, + Resolver = Resolve(x => x.Metadata.GetPixelHeight()), Description = "The height of the image in pixels if the asset is an image.", DeprecationReason = "Use 'metadata' field instead." }); @@ -175,7 +181,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "type", ResolvedType = AllTypes.NonNullAssetType, - Resolver = AssetResolvers.Type, + Resolver = Resolve(x => x.Type), Description = "The type of the image." }); @@ -183,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "metadataText", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.MetadataText, + Resolver = Resolve(x => x.MetadataText), Description = "The text representation of the metadata." }); @@ -191,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "tags", ResolvedType = null, - Resolver = AssetResolvers.Tags, + Resolver = Resolve(x => x.TagNames), Description = "The asset tags.", Type = AllTypes.NonNullTagsType }); @@ -205,18 +211,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = "The asset metadata." }); - if (model.CanGenerateAssetSourceUrl) + if (canGenerateSourceUrl) { AddField(new FieldType { Name = "sourceUrl", ResolvedType = AllTypes.NonNullString, - Resolver = AssetResolvers.SourceUrl, + Resolver = SourceUrl, Description = "The source url of the asset." }); } Description = "An asset"; } + + private static readonly IFieldResolver Url = Resolve((asset, _, context) => + { + return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString()); + }); + + private static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + { + return context.UrlGenerator.AssetSource(asset.AppId, asset.Id, asset.FileVersion); + }); + + private static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + { + return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type); + }); + + private static IFieldResolver Resolve(Func resolver) + { + return Resolvers.Sync(resolver); + } + + private static IFieldResolver Resolve(Func resolver) + { + return Resolvers.Sync(resolver); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs similarity index 59% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs index 458da1624..547db74d0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetsResultGraphType.cs @@ -5,35 +5,42 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Assets { - public sealed class AssetsResultGraphType : ObjectGraphType> + internal sealed class AssetsResultGraphType : ObjectGraphType> { - public AssetsResultGraphType(IGraphType assetType) + public AssetsResultGraphType(IGraphType assetsList) { Name = "AssetResultDto"; AddField(new FieldType { Name = "total", - ResolvedType = AllTypes.Int, - Resolver = AssetResolvers.ListTotal, + ResolvedType = AllTypes.NonNullInt, + Resolver = ResolveList(x => x.Total), Description = "The total count of assets." }); AddField(new FieldType { Name = "items", - Resolver = AssetResolvers.ListItems, - ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), + ResolvedType = assetsList, + Resolver = ResolveList(x => x), Description = "The assets." }); Description = "List of assets and total count of assets."; } + + private static IFieldResolver ResolveList(Func, T> resolver) + { + return Resolvers.Sync(resolver); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs deleted file mode 100644 index f796e9018..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs +++ /dev/null @@ -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 - { - 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."; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs similarity index 60% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index f3630783e..83e8ff1d6 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -14,16 +14,48 @@ using GraphQL.Types; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Validation; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public static class ContentActions { + private static readonly QueryArgument Id = new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The id of the content (usually GUID).", + DefaultValue = null, + ResolvedType = AllTypes.NonNullDomainId + }; + + private static readonly QueryArgument NewId = new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The optional custom content id.", + DefaultValue = null, + ResolvedType = AllTypes.String + }; + + private static readonly QueryArgument ExpectedVersion = new QueryArgument(AllTypes.None) + { + Name = "expectedVersion", + Description = "The expected version", + DefaultValue = EtagVersion.Any, + ResolvedType = AllTypes.Int + }; + + private static readonly QueryArgument Publish = new QueryArgument(AllTypes.None) + { + Name = "publish", + Description = "Set to true to autopublish content on create.", + DefaultValue = false, + ResolvedType = AllTypes.Boolean + }; + public static class Json { public static readonly QueryArguments Arguments = new QueryArguments @@ -37,9 +69,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types } }; - public static readonly ValueResolver Resolver = (value, c) => + public static readonly ValueResolver Resolver = (value, fieldContext, context) => { - if (c.Arguments.TryGetValue("path", out var p) && p is string path) + if (fieldContext.Arguments.TryGetValue("path", out var p) && p is string path) { value.TryGetByPath(path, out var result); @@ -65,13 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public static readonly QueryArguments Arguments = new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID).", - DefaultValue = null, - ResolvedType = AllTypes.NonNullDomainId - }, + Id, new QueryArgument(AllTypes.None) { Name = "version", @@ -85,19 +111,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var schemaIdValue = schemaId.ToString(); - return new FuncFieldResolver(c => + return Resolvers.Async(async (_, fieldContext, context) => { - var contentId = c.GetArgument("id"); + var contentId = fieldContext.GetArgument("id"); - var version = c.GetArgument("version"); + var version = fieldContext.GetArgument("version"); if (version >= 0) { - return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(schemaIdValue, contentId, version.Value); + return await context.FindContentAsync(schemaIdValue, contentId, version.Value); } else { - return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(contentId); + return await context.FindContentAsync(contentId); } }); } @@ -105,59 +131,54 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static class QueryOrReferencing { - private static QueryArguments? arguments; - - public static QueryArguments Arguments(int pageSize) + public static readonly QueryArguments Arguments = new QueryArguments { - return arguments ??= new QueryArguments + new QueryArgument(AllTypes.None) { - new QueryArgument(AllTypes.None) - { - Name = "top", - Description = $"Optional number of contents to take (Default: {pageSize}).", - DefaultValue = pageSize, - ResolvedType = AllTypes.Int - }, - new QueryArgument(AllTypes.None) - { - Name = "skip", - Description = "Optional number of contents 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 - }, - new QueryArgument(AllTypes.None) - { - Name = "search", - Description = "Optional OData full text search.", - DefaultValue = null, - ResolvedType = AllTypes.String - } - }; - } + Name = "top", + Description = $"Optional number of contents to take.", + DefaultValue = null, + ResolvedType = AllTypes.Int + }, + new QueryArgument(AllTypes.None) + { + Name = "skip", + Description = "Optional number of contents 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 + }, + new QueryArgument(AllTypes.None) + { + Name = "search", + Description = "Optional OData full text search.", + DefaultValue = null, + ResolvedType = AllTypes.String + } + }; public static IFieldResolver Query(DomainId schemaId) { var schemaIdValue = schemaId.ToString(); - return new FuncFieldResolver(c => + return Resolvers.Async(async (_, fieldContext, context) => { - var query = c.BuildODataQuery(); + var query = fieldContext.BuildODataQuery(); - return ((GraphQLExecutionContext)c.UserContext).QueryContentsAsync(schemaIdValue, query); + return await context.QueryContentsAsync(schemaIdValue, query); }); } @@ -165,13 +186,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var schemaIdValue = schemaId.ToString(); - return new FuncFieldResolver(c => + return Resolvers.Async(async (source, fieldContext, context) => { - var query = c.BuildODataQuery(); + var query = fieldContext.BuildODataQuery(); - var contentId = c.Source.Id; + var contentId = source.Id; - return ((GraphQLExecutionContext)c.UserContext).QueryReferencingContentsAsync(schemaIdValue, query, c.Source.Id); + return await context.QueryReferencingContentsAsync(schemaIdValue, query, source.Id); }); } } @@ -189,20 +210,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types DefaultValue = null, ResolvedType = new NonNullGraphType(inputType) }, - new QueryArgument(AllTypes.None) - { - Name = "publish", - Description = "Set to true to autopublish content.", - DefaultValue = false, - ResolvedType = AllTypes.Boolean - }, - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The optional custom content id.", - DefaultValue = null, - ResolvedType = AllTypes.String - } + Publish, NewId }; } @@ -234,13 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { return new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID)", - DefaultValue = null, - ResolvedType = AllTypes.NonNullString - }, + Id, new QueryArgument(AllTypes.None) { Name = "data", @@ -248,20 +250,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types DefaultValue = null, ResolvedType = new NonNullGraphType(inputType) }, - new QueryArgument(AllTypes.None) - { - Name = "publish", - Description = "Set to true to autopublish content on create.", - DefaultValue = false, - ResolvedType = AllTypes.Boolean - }, - new QueryArgument(AllTypes.None) - { - Name = "expectedVersion", - Description = "The expected version", - DefaultValue = EtagVersion.Any, - ResolvedType = AllTypes.Int - } + Publish, + ExpectedVersion }; } @@ -286,13 +276,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { return new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID)", - DefaultValue = string.Empty, - ResolvedType = AllTypes.NonNullString - }, + Id, new QueryArgument(AllTypes.None) { Name = "data", @@ -300,13 +284,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types DefaultValue = null, ResolvedType = new NonNullGraphType(inputType) }, - new QueryArgument(AllTypes.None) - { - Name = "expectedVersion", - Description = "The expected version", - DefaultValue = EtagVersion.Any, - ResolvedType = AllTypes.Int - } + ExpectedVersion }; } @@ -328,13 +306,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { return new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID)", - DefaultValue = string.Empty, - ResolvedType = AllTypes.NonNullString - }, + Id, new QueryArgument(AllTypes.None) { Name = "data", @@ -342,13 +314,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types DefaultValue = null, ResolvedType = new NonNullGraphType(inputType) }, - new QueryArgument(AllTypes.None) - { - Name = "expectedVersion", - Description = "The expected version", - DefaultValue = EtagVersion.Any, - ResolvedType = AllTypes.Int - } + ExpectedVersion }; } @@ -368,13 +334,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public static readonly QueryArguments Arguments = new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID)", - DefaultValue = null, - ResolvedType = AllTypes.NonNullString - }, + Id, new QueryArgument(AllTypes.None) { Name = "status", @@ -389,13 +349,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types DefaultValue = null, ResolvedType = AllTypes.Date }, - new QueryArgument(AllTypes.None) - { - Name = "expectedVersion", - Description = "The expected version", - DefaultValue = EtagVersion.Any, - ResolvedType = AllTypes.Int - } + ExpectedVersion }; public static IFieldResolver Resolver(NamedId appId, NamedId schemaId) @@ -415,20 +369,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public static readonly QueryArguments Arguments = new QueryArguments { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the content (usually GUID)", - DefaultValue = null, - ResolvedType = AllTypes.NonNullString - }, - new QueryArgument(AllTypes.None) - { - Name = "expectedVersion", - Description = "The expected version", - DefaultValue = EtagVersion.Any, - ResolvedType = AllTypes.Int - } + Id, + ExpectedVersion }; public static IFieldResolver Resolver(NamedId appId, NamedId schemaId) @@ -451,31 +393,29 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private static IFieldResolver ResolveAsync(NamedId appId, NamedId schemaId, Func action) { - return new FuncFieldResolver>(async c => + return Resolvers.Async(async (source, fieldContext, context) => { - var e = (GraphQLExecutionContext)c.UserContext; - try { - var command = action(c); + var command = action(fieldContext); command.AppId = appId; command.SchemaId = schemaId; - command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any); + command.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any); - var commandContext = await e.CommandBus.PublishAsync(command); + var commandContext = await context.CommandBus.PublishAsync(command); return commandContext.Result(); } catch (ValidationException ex) { - c.Errors.Add(new ExecutionError(ex.Message)); + fieldContext.Errors.Add(new ExecutionError(ex.Message)); throw; } catch (DomainException ex) { - c.Errors.Add(new ExecutionError(ex.Message)); + fieldContext.Errors.Add(new ExecutionError(ex.Message)); throw; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataFlatGraphType.cs similarity index 95% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataFlatGraphType.cs index 6fdac6a17..93fb6653f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataFlatGraphType.cs @@ -9,7 +9,7 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentDataFlatGraphType : ObjectGraphType { @@ -27,8 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = fieldName, Arguments = args, - Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name), ResolvedType = resolvedType, + Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name), Description = field.RawProperties.Hints }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataGraphType.cs similarity index 97% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataGraphType.cs index f5b9f8abf..d407801dd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataGraphType.cs @@ -10,7 +10,7 @@ using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentDataGraphType : ObjectGraphType { @@ -39,8 +39,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = partitionKey.EscapePartition(), Arguments = args, - Resolver = ContentResolvers.Partition(valueResolver, partitionKey), ResolvedType = resolvedType, + Resolver = ContentResolvers.Partition(valueResolver, partitionKey), Description = field.RawProperties.Hints }); } @@ -50,8 +50,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = fieldName, - Resolver = ContentResolvers.Field(field), ResolvedType = fieldGraphType, + Resolver = ContentResolvers.Field(field), Description = $"The {displayName} field." }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataInputGraphType.cs similarity index 94% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataInputGraphType.cs index b07492807..ec3b5fec3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentDataInputGraphType.cs @@ -10,7 +10,7 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentDataInputGraphType : InputObjectGraphType { @@ -38,10 +38,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types fieldGraphType.AddField(new FieldType { Name = partitionKey.EscapePartition(), - Resolver = null, ResolvedType = resolvedType, + Resolver = null, Description = field.RawProperties.Hints - }).WithSourceName( partitionKey); + }).WithSourceName(partitionKey); } 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 { Name = fieldName, - Resolver = null, ResolvedType = fieldGraphType, + Resolver = null, Description = $"The {displayName} field." }).WithSourceName(field.Name); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs new file mode 100644 index 000000000..299fb2f0a --- /dev/null +++ b/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(Func resolver) + { + return Resolvers.Sync(resolver); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs similarity index 62% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs index fe13f473f..ac009f6f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentGraphType.cs @@ -12,7 +12,7 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentGraphType : ObjectGraphType { @@ -27,71 +27,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = schemaType.SafeTypeName(); - AddField(new FieldType - { - Name = "id", - ResolvedType = AllTypes.NonNullString, - Resolver = EntityResolvers.Id, - Description = $"The id of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "version", - ResolvedType = AllTypes.NonNullInt, - Resolver = EntityResolvers.Version, - Description = $"The version of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "created", - ResolvedType = AllTypes.NonNullDate, - Resolver = EntityResolvers.Created, - Description = $"The date and time when the {schemaName} content has been created." - }); - - AddField(new FieldType - { - Name = "createdBy", - ResolvedType = AllTypes.NonNullString, - Resolver = EntityResolvers.CreatedBy, - Description = $"The user that has created the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "lastModified", - ResolvedType = AllTypes.NonNullDate, - Resolver = EntityResolvers.LastModified, - Description = $"The date and time when the {schemaName} content has been modified last." - }); - - AddField(new FieldType - { - Name = "lastModifiedBy", - ResolvedType = AllTypes.NonNullString, - Resolver = EntityResolvers.LastModifiedBy, - Description = $"The user that has updated the {schemaName} content last." - }); - - AddField(new FieldType - { - Name = "status", - ResolvedType = AllTypes.NonNullString, - Resolver = ContentResolvers.Status, - Description = $"The the status of the {schemaName} content." - }); - - AddField(new FieldType - { - Name = "statusColor", - ResolvedType = AllTypes.NonNullString, - Resolver = ContentResolvers.StatusColor, - Description = $"The color status of the {schemaName} content." - }); + AddField(ContentFields.Id); + AddField(ContentFields.Version); + AddField(ContentFields.Created); + AddField(ContentFields.CreatedBy); + AddField(ContentFields.LastModified); + AddField(ContentFields.LastModifiedBy); + AddField(ContentFields.Status); + AddField(ContentFields.StatusColor); - Interface(); + AddResolvedInterface(ContentInterfaceGraphType.Instance); Description = $"The structure of a {schemaName} content type."; @@ -103,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return value is IContentEntity content && content.SchemaId?.Id == schemaId; } - public void Initialize(IGraphModel model, ISchemaEntity schema, IEnumerable all, int pageSize) + public void Initialize(IGraphModel model, ISchemaEntity schema, IEnumerable all) { var schemaType = schema.TypeName(); var schemaName = schema.DisplayName(); @@ -113,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = "url", ResolvedType = AllTypes.NonNullString, Resolver = ContentResolvers.Url, - Description = $"The url to the the {schemaName} content." + Description = $"The url to the content." }); var contentDataType = new ContentDataGraphType(schema, schemaName, schemaType, model); @@ -125,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = "data", ResolvedType = new NonNullGraphType(contentDataType), Resolver = ContentResolvers.Data, - Description = $"The data of the {schemaName} content." + Description = $"The data of the content." }); } @@ -138,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = "flatData", ResolvedType = new NonNullGraphType(contentDataTypeFlat), Resolver = ContentResolvers.FlatData, - Description = $"The flat data of the {schemaName} content." + Description = $"The flat data of the content." }); } @@ -150,18 +95,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types var contentType = model.GetContentType(referencingId); - AddReferencingQueries(referencingId, referencingType, referencingName, contentType, pageSize); + AddReferencingQueries(referencingId, referencingType, referencingName, contentType); } } - private void AddReferencingQueries(DomainId referencingId, string referencingType, string referencingName, IGraphType contentType, int pageSize) + private void AddReferencingQueries(DomainId referencingId, string referencingType, string referencingName, IGraphType contentType) { var resolver = ContentActions.QueryOrReferencing.Referencing(referencingId); AddField(new FieldType { Name = $"referencing{referencingType}Contents", - Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), Resolver = resolver, Description = $"Query {referencingName} content items." @@ -170,7 +115,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = $"referencing{referencingType}ContentsWithTotal", - Arguments = ContentActions.QueryOrReferencing.Arguments(pageSize), + Arguments = ContentActions.QueryOrReferencing.Arguments, ResolvedType = new ContentsResultGraphType(referencingType, referencingName, contentType), Resolver = resolver, Description = $"Query {referencingName} content items with total count." diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentInterfaceGraphType.cs new file mode 100644 index 000000000..3d9b845d6 --- /dev/null +++ b/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 + { + 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."; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs similarity index 62% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs index 4239e17c5..0f740476e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentResolvers.cs @@ -15,17 +15,17 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { - public static class ContentResolvers + internal static class ContentResolvers { public static IFieldResolver NestedValue(ValueResolver valueResolver, string key) { - return new FuncFieldResolver(c => + return Resolvers.Sync((source, fieldContext, context) => { - if (c.Source.TryGetValue(key, out var value)) + if (source.TryGetValue(key, out var value)) { - return valueResolver(value, c); + return valueResolver(value, fieldContext, context); } return null; @@ -34,11 +34,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static IFieldResolver Partition(ValueResolver valueResolver, string key) { - return new FuncFieldResolver(c => + return Resolvers.Sync((source, fieldContext, context) => { - if (c.Source.TryGetValue(key, out var value) && value != null) + if (source.TryGetValue(key, out var value) && value != null) { - return valueResolver(value, c); + return valueResolver(value, fieldContext, context); } return null; @@ -47,11 +47,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static IFieldResolver FlatPartition(ValueResolver valueResolver, string key) { - return new FuncFieldResolver(c => + return Resolvers.Sync((source, fieldContext, context) => { - if (c.Source.TryGetValue(key, out var value) && value != null) + if (source.TryGetValue(key, out var value) && value != null) { - return valueResolver(value, c); + return valueResolver(value, fieldContext, context); } return null; @@ -62,9 +62,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var fieldName = field.Name; - return new FuncFieldResolver?>(c => + return Resolvers.Sync?>(source => { - return c.Source?.GetOrDefault(fieldName); + return source?.GetOrDefault(fieldName); }); } @@ -83,24 +83,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types }); public static readonly IFieldResolver Data = Resolve(x => x.Data); - public static readonly IFieldResolver Status = Resolve(x => x.Status.Name.ToUpperInvariant()); - public static readonly IFieldResolver StatusColor = Resolve(x => x.StatusColor); + public static readonly IFieldResolver ListTotal = ResolveList(x => x.Total); + public static readonly IFieldResolver ListItems = ResolveList(x => x); - private static IFieldResolver Resolve(Func action) + private static IFieldResolver Resolve(Func resolver) { - return new FuncFieldResolver(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext)); + return Resolvers.Sync(resolver); } - private static IFieldResolver Resolve(Func action) + private static IFieldResolver Resolve(Func resolver) { - return new FuncFieldResolver(c => action(c.Source)); + return Resolvers.Sync(resolver); } - private static IFieldResolver ResolveList(Func, T> action) + private static IFieldResolver ResolveList(Func, T> resolver) { - return new FuncFieldResolver, object?>(c => action(c.Source)); + return Resolvers.Sync(resolver); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs similarity index 96% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs index f5e682425..ba3aced69 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentUnionGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentUnionGraphType.cs @@ -10,7 +10,7 @@ using System.Linq; using GraphQL.Types; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentUnionGraphType : UnionGraphType { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentsResultGraphType.cs similarity index 94% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentsResultGraphType.cs index 1d01f9959..fe91f09e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentsResultGraphType.cs @@ -8,7 +8,7 @@ using GraphQL.Types; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents { public sealed class ContentsResultGraphType : ObjectGraphType> { @@ -19,16 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "total", - Resolver = ContentResolvers.ListTotal, ResolvedType = AllTypes.NonNullInt, + Resolver = ContentResolvers.ListTotal, Description = $"The total number of {schemaName} items." }); AddField(new FieldType { Name = "items", - Resolver = ContentResolvers.ListItems, ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), + Resolver = ContentResolvers.ListItems, Description = $"The {schemaName} items." }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs index b8bc9a91b..eb8536ea9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs @@ -10,9 +10,12 @@ using System.Collections.Generic; using System.Linq; using GraphQL; using GraphQL.Types; +using GraphQL.Utilities; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.ObjectPool; using Squidex.Text; +using GraphQLSchema = GraphQL.Types.Schema; #pragma warning disable RECS0015 // If an extension method is called as static method convert it to method syntax @@ -63,25 +66,53 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static string BuildODataQuery(this IResolveFieldContext context) { - var odataQuery = "?" + - string.Join("&", - context.Arguments - .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => $"${x.Key}={x.Value}")); - - return odataQuery; + var sb = DefaultPools.StringBuilder.Get(); + try + { + sb.Append('?'); + + foreach (var argument in context.Arguments) + { + var value = argument.Value?.ToString(); + + if (!string.IsNullOrWhiteSpace(value)) + { + if (sb.Length > 1) + { + sb.Append('&'); + } + + sb.Append('$'); + sb.Append(argument.Key); + sb.Append('='); + sb.Append(value); + } + } + + return sb.ToString(); + } + finally + { + DefaultPools.StringBuilder.Return(sb); + } } public static FieldType WithSourceName(this FieldType field, object value) { - field.Metadata["sourceName"] = value; + if (field is MetadataProvider metadataProvider) + { + metadataProvider.Metadata = new Dictionary + { + ["sourceName"] = value + }; + } return field; } public static string GetSourceName(this FieldType field) { - return field.Metadata.GetOrAddDefault("sourceName") as string ?? field.Name; + return field.GetMetadata("sourceName", string.Empty); } public static IGraphType Flatten(this QueryArgument type) @@ -98,5 +129,77 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return type; } + + public static void CleanupMetadata(this GraphQLSchema schema) + { + var targets = new HashSet(ReferenceEqualityComparer.Instance); + + foreach (var type in schema.AllTypes) + { + FindTargets(type, targets); + } + + foreach (var target in targets.OfType()) + { + var metadata = target.Metadata; + + if (metadata != null && metadata.Count == 0) + { + target.Metadata = null; + } + } + } + + private static void FindTargets(IGraphType type, HashSet 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); + } + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeFactory.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeFactory.cs new file mode 100644 index 000000000..432e759e9 --- /dev/null +++ b/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 asset; + private readonly Lazy assetsList; + private readonly Lazy assetsResult; + private readonly Lazy findAsset; + private readonly Lazy queryAssets; + private readonly Lazy 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(() => + { + return new AssetGraphType(urlGenerator.CanGenerateAssetSourceUrl); + }); + + assetsList = new Lazy(() => + { + return new NonNullGraphType(new ListGraphType(new NonNullGraphType(Asset))); + }); + + assetsResult = new Lazy(() => + { + return new AssetsResultGraphType(AssetsList); + }); + + findAsset = new Lazy(() => + { + return new FieldType + { + Name = "findAsset", + Arguments = AssetActions.Find.Arguments, + ResolvedType = Asset, + Resolver = AssetActions.Find.Resolver, + Description = "Find an asset by id." + }; + }); + + queryAssets = new Lazy(() => + { + return new FieldType + { + Name = "queryAssets", + Arguments = AssetActions.Query.Arguments, + ResolvedType = AssetsList, + Resolver = AssetActions.Query.Resolver, + Description = "Get assets." + }; + }); + + queryAssetsWithTotal = new Lazy(() => + { + return new FieldType + { + Name = "queryAssetsWithTotal", + Arguments = AssetActions.Query.Arguments, + ResolvedType = AssetsResult, + Resolver = AssetActions.Query.Resolver, + Description = "Get assets and total count." + }; + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs index 32e65dd02..e0be3964a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GraphQLTypeVisitor.cs @@ -10,19 +10,20 @@ using System.Linq; using GraphQL; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public delegate object ValueResolver(IJsonValue value, IResolveFieldContext context); + public delegate object ValueResolver(IJsonValue value, IResolveFieldContext fieldContext, GraphQLExecutionContext context); internal sealed class GraphQLTypeVisitor : IFieldVisitor<(IGraphType?, ValueResolver?, QueryArguments?), GraphQLTypeVisitor.Args> { - private static readonly ValueResolver NoopResolver = (value, c) => value; + private static readonly ValueResolver NoopResolver = (value, fieldContext, contex) => value; + private readonly Dictionary schemaTypes; - private readonly IGraphType assetListType; private readonly IGraphModel model; public readonly struct Args @@ -38,13 +39,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types } } - public GraphQLTypeVisitor( - Dictionary schemaTypes, - IGraphModel model, - IGraphType assetListType) + public GraphQLTypeVisitor(Dictionary schemaTypes, IGraphModel model) { - this.assetListType = assetListType; this.model = model; + this.schemaTypes = schemaTypes; } @@ -115,14 +113,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets() { - var resolver = new ValueResolver((value, c) => + var resolver = new ValueResolver((value, _, context) => { - var context = (GraphQLExecutionContext)c.UserContext; - return context.GetReferencedAssetsAsync(value); }); - return (assetListType, resolver, null); + return (model.TypeFactory.AssetsList, resolver, null); } private (IGraphType?, ValueResolver?, QueryArguments?) ResolveReferences(IField field, Args args) @@ -141,10 +137,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types contentType = union; } - var resolver = new ValueResolver((value, c) => + var resolver = new ValueResolver((value, _, context) => { - var context = (GraphQLExecutionContext)c.UserContext; - return context.GetReferencedContentsAsync(value); }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs index b3d28e6f8..2099792e7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs @@ -8,6 +8,7 @@ using System.Linq; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.Json.Objects; @@ -30,14 +31,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types if (resolvedType != null && valueResolver != null) { - var resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name); - AddField(new FieldType { Name = nestedName, Arguments = args, ResolvedType = resolvedType, - Resolver = resolver, + Resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name), Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." }); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs index f6bd5ff2e..83acda2b1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs @@ -32,8 +32,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = nestedName, - Resolver = null, ResolvedType = resolvedType, + Resolver = null, Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." }).WithSourceName(nestedField.Name); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/Converters.cs similarity index 96% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/Converters.cs index 12900b1f4..f31b46ed2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/Converters.cs @@ -10,9 +10,9 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public static class Converters + internal static class Converters { public static ContentData ToContentData(this IDictionary source, IComplexGraphType type) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs similarity index 84% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs index 287ec4f20..1b14da417 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntityResolvers.cs @@ -8,9 +8,9 @@ using System; using GraphQL.Resolvers; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public static class EntityResolvers + internal static class EntityResolvers { public static readonly IFieldResolver Id = Resolve(x => x.Id.ToString()); public static readonly IFieldResolver Created = Resolve(x => x.Created); @@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IFieldResolver LastModifiedBy = Resolve(x => x.LastModifiedBy.ToString()); public static readonly IFieldResolver Version = Resolve(x => x.Version); - private static IFieldResolver Resolve(Func action) + private static IFieldResolver Resolve(Func resolver) { - return new FuncFieldResolver(c => action(c.Source)); + return Resolvers.Sync(resolver); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs similarity index 84% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs index 976732567..5e87d5235 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/EntitySavedGraphType.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils { - public sealed class EntitySavedGraphType : ObjectGraphType + internal sealed class EntitySavedGraphType : ObjectGraphType { public static readonly IGraphType Nullable = new EntitySavedGraphType(); @@ -34,10 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils private static IFieldResolver ResolveVersion() { - return new FuncFieldResolver(x => - { - return x.Source.Version; - }); + return Resolvers.Sync(x => x.Version); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/GeolocationInputGraphType.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/GeolocationInputGraphType.cs index 62054a074..1e4f0e03e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/GeolocationInputGraphType.cs @@ -9,7 +9,7 @@ using GraphQL.Types; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils { - public sealed class GeolocationInputGraphType : InputObjectGraphType + internal sealed class GeolocationInputGraphType : InputObjectGraphType { public static readonly IGraphType Nullable = new GeolocationInputGraphType(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantConverter.cs similarity index 85% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantConverter.cs index 4c3e346df..dba150ecb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantConverter.cs @@ -9,9 +9,9 @@ using GraphQL.Language.AST; using GraphQL.Types; using NodaTime; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class InstantConverter : IAstFromValueConverter + internal sealed class InstantConverter : IAstFromValueConverter { public static readonly InstantConverter Instance = new InstantConverter(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs similarity index 89% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs index 2ba699892..45003298f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantGraphType.cs @@ -9,9 +9,9 @@ using GraphQL.Language.AST; using GraphQL.Types; using NodaTime.Text; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class InstantGraphType : DateGraphType + internal sealed class InstantGraphType : DateGraphType { public override object Serialize(object value) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantValueNode.cs similarity index 82% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantValueNode.cs index b623c39a3..d9819e9a0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/InstantValueNode.cs @@ -8,9 +8,9 @@ using GraphQL.Language.AST; using NodaTime; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class InstantValueNode : ValueNode + internal sealed class InstantValueNode : ValueNode { public InstantValueNode(Instant value) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonConverter.cs similarity index 93% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonConverter.cs index c11397608..9db63ae8d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonConverter.cs @@ -10,9 +10,9 @@ using GraphQL.Language.AST; using GraphQL.Types; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class JsonConverter : IAstFromValueConverter + internal sealed class JsonConverter : IAstFromValueConverter { public static readonly JsonConverter Instance = new JsonConverter(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs similarity index 88% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs index c59a013c2..215dc7f23 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonGraphType.cs @@ -8,9 +8,9 @@ using GraphQL.Language.AST; using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class JsonGraphType : ScalarGraphType + internal sealed class JsonGraphType : ScalarGraphType { public JsonGraphType() { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs similarity index 83% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs index 7b784ea65..3f566204d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/JsonValueNode.cs @@ -8,9 +8,9 @@ using GraphQL.Language.AST; using Squidex.Infrastructure.Json.Objects; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class JsonValueNode : ValueNode + internal sealed class JsonValueNode : ValueNode { public JsonValueNode(IJsonValue value) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/NoopGraphType.cs similarity index 88% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/NoopGraphType.cs index 8b8c3ceea..e64a94e9b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/NoopGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Primitives/NoopGraphType.cs @@ -8,9 +8,9 @@ using GraphQL.Language.AST; using GraphQL.Types; -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Primitives { - public sealed class NoopGraphType : ScalarGraphType + internal sealed class NoopGraphType : ScalarGraphType { public NoopGraphType(string name) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Resolvers.cs new file mode 100644 index 000000000..77532d2e7 --- /dev/null +++ b/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(Func resolver) + { + return new SyncResolver((source, context, execution) => resolver(source)); + } + + public static IFieldResolver Sync(Func resolver) + { + return new SyncResolver(resolver); + } + + public static IFieldResolver Async(Func> resolver) + { + return new AsyncResolver((source, context, execution) => resolver(source)); + } + + public static IFieldResolver Async(Func> resolver) + { + return new AsyncResolver(resolver); + } + + private sealed class SyncResolver : IFieldResolver, IFieldResolver + { + private readonly Func resolver; + + public SyncResolver(Func 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 : IFieldResolver>, IFieldResolver + { + private readonly Func> resolver; + + public AsyncResolver(Func> resolver) + { + this.resolver = resolver; + } + + public async Task 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)!; + } + } + } +} diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs index 4732f9646..45651c669 100644 --- a/backend/src/Squidex/Config/Domain/QueryServices.cs +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Web.Services; namespace Squidex.Config.Domain @@ -29,8 +30,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .AsSelf(); services.AddSingletonAs() .As(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index db53d8983..0602afc7d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Domain.Apps.Entities.Contents.TestData; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -157,6 +158,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL var dataLoaderContext = new DataLoaderContextAccessor(); + var urlGenerator = new FakeUrlGenerator(); + services = new Dictionary { [typeof(IAppProvider)] = appProvider, @@ -164,11 +167,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL [typeof(ICommandBus)] = testBase.commandBus, [typeof(IContentQueryService)] = testBase.contentQuery, [typeof(IDataLoaderContextAccessor)] = dataLoaderContext, - [typeof(IOptions)] = Options.Create(new AssetOptions()), - [typeof(IOptions)] = Options.Create(new ContentOptions()), + [typeof(IUrlGenerator)] = urlGenerator, [typeof(ISemanticLog)] = A.Fake(), - [typeof(IUrlGenerator)] = new FakeUrlGenerator(), - [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext) + [typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext), + [typeof(GraphQLTypeFactory)] = new GraphQLTypeFactory(urlGenerator) }; }