diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs index e57303730..6cad54c40 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs @@ -7,7 +7,6 @@ using System; using System.Threading.Tasks; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Translations; using Squidex.Text; 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 6437d3fb5..51f687712 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs @@ -8,7 +8,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using GraphQL; +using GraphQL.Utilities; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core; @@ -22,9 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IDependencyResolver resolver; + private readonly IServiceProvider resolver; - public CachingGraphQLService(IMemoryCache cache, IDependencyResolver resolver) + public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver) : base(cache) { Guard.NotNull(resolver, nameof(resolver)); @@ -87,24 +87,24 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { entry.AbsoluteExpirationRelativeToNow = CacheDuration; - var allSchemas = await resolver.Resolve().GetSchemasAsync(app.Id); + var allSchemas = await resolver.GetRequiredService().GetSchemasAsync(app.Id); return new GraphQLModel(app, allSchemas, GetPageSizeForContents(), GetPageSizeForAssets(), - resolver.Resolve()); + resolver.GetRequiredService()); }); } private int GetPageSizeForContents() { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + return resolver.GetRequiredService>().Value.DefaultPageSizeGraphQl; } private int GetPageSizeForAssets() { - return resolver.Resolve>().Value.DefaultPageSizeGraphQl; + return resolver.GetRequiredService>().Value.DefaultPageSizeGraphQl; } private static object CreateCacheKey(Guid appId, string 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 077f70b54..7510ad28a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -11,11 +11,12 @@ using System.Linq; using System.Threading.Tasks; using GraphQL; using GraphQL.DataLoader; +using GraphQL.Utilities; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Log; @@ -24,34 +25,38 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL public sealed class GraphQLExecutionContext : QueryExecutionContext { private static readonly List EmptyAssets = new List(); - private static readonly List EmptyContents = new List(); + private static readonly List EmptyContents = new List(); private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; - private readonly IDependencyResolver resolver; + private readonly IServiceProvider resolver; public IUrlGenerator UrlGenerator { get; } + public ICommandBus CommandBus { get; } + public ISemanticLog Log { get; } - public GraphQLExecutionContext(Context context, IDependencyResolver resolver) + public GraphQLExecutionContext(Context context, IServiceProvider resolver) : base(context .WithoutCleanup() .WithoutContentEnrichment(), - resolver.Resolve(), - resolver.Resolve()) + resolver.GetRequiredService(), + resolver.GetRequiredService()) { - UrlGenerator = resolver.Resolve(); + UrlGenerator = resolver.GetRequiredService(); + + CommandBus = resolver.GetRequiredService(); - dataLoaderContextAccessor = resolver.Resolve(); + dataLoaderContextAccessor = resolver.GetRequiredService(); this.resolver = resolver; } public void Setup(ExecutionOptions execution) { - var loader = resolver.Resolve(); + var loader = resolver.GetRequiredService(); execution.Listeners.Add(loader); - execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve())); + execution.FieldMiddleware.Use(Middlewares.Logging(resolver.GetRequiredService())); execution.FieldMiddleware.Use(Middlewares.Errors()); execution.UserContext = this; @@ -61,14 +66,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { var dataLoader = GetAssetsLoader(); - return await dataLoader.LoadAsync(id); + return await dataLoader.LoadAsync(id).GetResultAsync(); } public async Task FindContentAsync(Guid id) { var dataLoader = GetContentsLoader(); - return await dataLoader.LoadAsync(id); + return await dataLoader.LoadAsync(id).GetResultAsync(); } public Task> GetReferencedAssetsAsync(IJsonValue value) @@ -85,13 +90,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return LoadManyAsync(dataLoader, ids); } - public Task> GetReferencedContentsAsync(IJsonValue value) + public Task> GetReferencedContentsAsync(IJsonValue value) { var ids = ParseIds(value); if (ids == null) { - return Task.FromResult>(EmptyContents); + return Task.FromResult>(EmptyContents); } var dataLoader = GetContentsLoader(); @@ -110,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }); } - private IDataLoader GetContentsLoader() + private IDataLoader GetContentsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader(nameof(GetContentsLoader), + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader(nameof(GetContentsLoader), async batch => { var result = await GetReferencedContentsAsync(new List(batch)); @@ -123,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL private static async Task> LoadManyAsync(IDataLoader dataLoader, ICollection keys) where T : class { - var contents = await Task.WhenAll(keys.Select(dataLoader.LoadAsync)); + var contents = await Task.WhenAll(keys.Select(x => dataLoader.LoadAsync(x).GetResultAsync())); return contents.NotNull().ToList(); } 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 ab066b73b..96ba556df 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs @@ -15,7 +15,6 @@ using GraphQL.Types; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; 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.GraphQL.Types.Utils; using Squidex.Domain.Apps.Entities.Schemas; @@ -58,6 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL graphQLSchema = BuildSchema(this, pageSizeContents, pageSizeAssets, allSchemas); graphQLSchema.RegisterValueConverter(JsonConverter.Instance); + graphQLSchema.RegisterValueConverter(InstantConverter.Instance); InitializeContentTypes(); } @@ -87,48 +87,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { var schema = new GraphQLSchema { - Query = new AppQueriesGraphType(model, pageSizeContents, pageSizeAssets, schemas) + Query = + new AppQueriesGraphType( + model, + pageSizeContents, + pageSizeAssets, + schemas + ), + Mutation = new AppMutationsGraphType(model, schemas) }; return schema; } - public IFieldResolver ResolveAssetUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.AssetContent(c.Source.Id); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetSourceUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.AssetSource(c.Source.Id, c.Source.FileVersion); - }); - - return resolver; - } - - public IFieldResolver ResolveAssetThumbnailUrl() - { - var resolver = new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return context.UrlGenerator.AssetThumbnail(c.Source.Id, c.Source.Type); - }); - - return resolver; - } - public IFieldResolver ResolveContentUrl(ISchemaEntity schema) { var resolver = new FuncFieldResolver(c => @@ -146,6 +117,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return partitionResolver(key); } + public IGraphType? GetInputGraphType(ISchemaEntity schema, IField field, string fieldName) + { + return field.Accept(new InputFieldVisitor(schema, this, fieldName)); + } + public (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName) { return field.Accept(new QueryGraphTypeVisitor(schema, contentTypes, this, assetListType, fieldName)); 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 01e8ee4fc..e0e099331 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; @@ -21,18 +20,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL IFieldPartitioning ResolvePartition(Partitioning key); - IFieldResolver ResolveAssetUrl(); - - IFieldResolver ResolveAssetSourceUrl(); - - IFieldResolver ResolveAssetThumbnailUrl(); - - IFieldResolver ResolveContentUrl(ISchemaEntity schema); - IObjectGraphType GetAssetType(); IObjectGraphType GetContentType(Guid schemaId); + IGraphType? GetInputGraphType(ISchemaEntity schema, IField field, string fieldName); + (IGraphType?, ValueResolver?, QueryArguments?) GetGraphType(ISchemaEntity schema, IField field, string fieldName); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs index 03adccccc..fccdeff9e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs @@ -8,6 +8,7 @@ using System; using GraphQL; using GraphQL.Instrumentation; +using GraphQL.Types; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -15,11 +16,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public static class Middlewares { - public static Func Logging(ISemanticLog log) + public static Func Logging(ISemanticLog log) { Guard.NotNull(log, nameof(log)); - return next => + return (_, next) => { return async context => { @@ -40,9 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL }; } - public static Func Errors() + public static Func Errors() { - return next => + return (_, next) => { return async context => { 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 57ac5c124..ae8e243c7 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 @@ -14,18 +14,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { public static class AllTypes { - public const string PathName = "path"; - public static readonly Type None = typeof(NoopGraphType); public static readonly Type NonNullTagsType = typeof(NonNullGraphType>>); public static readonly IGraphType Int = new IntGraphType(); - public static readonly IGraphType Guid = new GuidGraphType2(); + public static readonly IGraphType Long = new LongGraphType(); + + public static readonly IGraphType Guid = new GuidGraphType(); public static readonly IGraphType Date = new InstantGraphType(); + public static readonly IGraphType Tags = new ListGraphType(new NonNullGraphType(new StringGraphType())); + public static readonly IGraphType Json = new JsonGraphType(); public static readonly IGraphType Float = new FloatGraphType(); @@ -36,8 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType AssetType = new EnumerationGraphType(); + public static readonly IGraphType References = new ListGraphType(new NonNullGraphType(new StringGraphType())); + public static readonly IGraphType NonNullInt = new NonNullGraphType(Int); + public static readonly IGraphType NonNullLong = new NonNullGraphType(Long); + public static readonly IGraphType NonNullGuid = new NonNullGraphType(Guid); public static readonly IGraphType NonNullDate = new NonNullGraphType(Date); @@ -63,13 +69,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public static readonly IGraphType NoopTags = new NoopGraphType("TagsScalar"); public static readonly IGraphType NoopGeolocation = new NoopGraphType("GeolocationScalar"); - - public static readonly QueryArguments PathArguments = new QueryArguments(new QueryArgument(None) - { - Name = PathName, - Description = "The path to the json value", - DefaultValue = null, - ResolvedType = String - }); } } 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 new file mode 100644 index 000000000..8c86d17ee --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class AppMutationsGraphType : ObjectGraphType + { + public AppMutationsGraphType(IGraphModel model, IEnumerable schemas) + { + foreach (var schema in schemas) + { + var schemaId = schema.NamedId(); + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + + var contentType = model.GetContentType(schema.Id); + + var inputType = new ContentDataInputGraphType(schema, schemaName, schemaType, model); + + AddContentCreate(schemaId, schemaType, schemaName, inputType, contentType); + AddContentUpdate(schemaType, schemaName, inputType, contentType); + AddContentPatch(schemaType, schemaName, inputType, contentType); + AddContentChangeStatus(schemaType, schemaName, contentType); + AddContentDelete(schemaType, schemaName); + } + + Description = "The app mutations."; + } + + private void AddContentCreate(NamedId schemaId, string schemaType, string schemaName, ContentDataInputGraphType inputType, IGraphType contentType) + { + AddField(new FieldType + { + Name = $"create{schemaType}Content", + Arguments = ContentActions.Create.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.Create.Resolver(schemaId), + Description = $"Creates an {schemaName} content." + }); + } + + private void AddContentUpdate(string schemaType, string schemaName, ContentDataInputGraphType inputType, IGraphType contentType) + { + AddField(new FieldType + { + Name = $"update{schemaType}Content", + Arguments = ContentActions.UpdateOrPatch.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.UpdateOrPatch.Update, + Description = $"Update an {schemaName} content by id." + }); + } + + private void AddContentPatch(string schemaType, string schemaName, ContentDataInputGraphType inputType, IGraphType contentType) + { + AddField(new FieldType + { + Name = $"patch{schemaType}Content", + Arguments = ContentActions.UpdateOrPatch.Arguments(inputType), + ResolvedType = contentType, + Resolver = ContentActions.UpdateOrPatch.Patch, + Description = $"Patch an {schemaName} content by id." + }); + } + + private void AddContentChangeStatus(string schemaType, string schemaName, IGraphType contentType) + { + AddField(new FieldType + { + Name = $"publish{schemaType}Content", + Arguments = ContentActions.ChangeStatus.Arguments, + ResolvedType = contentType, + Resolver = ContentActions.ChangeStatus.Resolver, + Description = $"Publish a {schemaName} content." + }); + } + + private void AddContentDelete(string schemaType, string schemaName) + { + AddField(new FieldType + { + Name = $"delete{schemaType}Content", + Arguments = ContentActions.Delete.Arguments, + ResolvedType = EntitySavedGraphType.NonNull, + Resolver = ContentActions.Delete.Resolver, + Description = $"Delete an {schemaName} content." + }); + } + } +} 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 b079e203e..517de2bc1 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,9 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Entities.Schemas; @@ -44,14 +41,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "findAsset", - Arguments = CreateAssetFindArguments(), + Arguments = AssetActions.Find.Arguments, ResolvedType = assetType, - Resolver = ResolveAsync((c, e) => - { - var assetId = c.GetArgument("id"); - - return e.FindAssetAsync(assetId); - }), + Resolver = AssetActions.Find.Resolver, Description = "Find an asset by id." }); } @@ -61,204 +53,57 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = $"find{schemaType}Content", - Arguments = CreateContentFindArguments(schemaName), + Arguments = ContentActions.Find.Arguments, ResolvedType = contentType, - Resolver = ResolveAsync((c, e) => - { - var contentId = c.GetArgument("id"); - - return e.FindContentAsync(contentId); - }), + Resolver = ContentActions.Find.Resolver, Description = $"Find an {schemaName} content by id." }); } private void AddAssetsQueries(IGraphType assetType, int pageSize) { + var resolver = AssetActions.Query.Resolver; + AddField(new FieldType { Name = "queryAssets", - Arguments = CreateAssetQueryArguments(pageSize), + Arguments = AssetActions.Query.Arguments(pageSize), ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), - Resolver = ResolveAsync((c, e) => - { - var assetQuery = BuildODataQuery(c); - - return e.QueryAssetsAsync(assetQuery); - }), + Resolver = resolver, Description = "Get assets." }); AddField(new FieldType { Name = "queryAssetsWithTotal", - Arguments = CreateAssetQueryArguments(pageSize), + Arguments = AssetActions.Query.Arguments(pageSize), ResolvedType = new AssetsResultGraphType(assetType), - Resolver = ResolveAsync((c, e) => - { - var assetQuery = BuildODataQuery(c); - - return e.QueryAssetsAsync(assetQuery); - }), + Resolver = resolver, Description = "Get assets and total count." }); } private void AddContentQueries(Guid schemaId, string schemaType, string schemaName, IGraphType contentType, int pageSize) { + var resolver = ContentActions.Query.Resolver(schemaId); + AddField(new FieldType { Name = $"query{schemaType}Contents", - Arguments = CreateContentQueryArguments(pageSize), + Arguments = ContentActions.Query.Arguments(pageSize), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), - Resolver = ResolveAsync((c, e) => - { - var contentQuery = BuildODataQuery(c); - - return e.QueryContentsAsync(schemaId.ToString(), contentQuery); - }), + Resolver = resolver, Description = $"Query {schemaName} content items." }); AddField(new FieldType { Name = $"query{schemaType}ContentsWithTotal", - Arguments = CreateContentQueryArguments(pageSize), + Arguments = ContentActions.Query.Arguments(pageSize), ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType), - Resolver = ResolveAsync((c, e) => - { - var contentQuery = BuildODataQuery(c); - - return e.QueryContentsAsync(schemaId.ToString(), contentQuery); - }), + Resolver = resolver, Description = $"Query {schemaName} content items with total count." }); } - - private static QueryArguments CreateAssetFindArguments() - { - return new QueryArguments - { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = "The id of the asset (GUID).", - DefaultValue = string.Empty, - ResolvedType = AllTypes.NonNullGuid - } - }; - } - - private static QueryArguments CreateContentFindArguments(string schemaName) - { - return new QueryArguments - { - new QueryArgument(AllTypes.None) - { - Name = "id", - Description = $"The id of the {schemaName} content (GUID)", - DefaultValue = string.Empty, - ResolvedType = AllTypes.NonNullGuid - } - }; - } - - private static QueryArguments CreateAssetQueryArguments(int pageSize) - { - return 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 = string.Empty, - ResolvedType = AllTypes.String - }, - new QueryArgument(AllTypes.None) - { - Name = "orderby", - Description = "Optional OData order definition.", - DefaultValue = string.Empty, - ResolvedType = AllTypes.String - } - }; - } - - private static QueryArguments CreateContentQueryArguments(int pageSize) - { - return new QueryArguments - { - 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 = string.Empty, - ResolvedType = AllTypes.String - }, - new QueryArgument(AllTypes.None) - { - Name = "search", - Description = "Optional OData full text search.", - DefaultValue = string.Empty, - ResolvedType = AllTypes.String - }, - new QueryArgument(AllTypes.None) - { - Name = "orderby", - Description = "Optional OData order definition.", - DefaultValue = string.Empty, - ResolvedType = AllTypes.String - } - }; - } - - private static string BuildODataQuery(ResolveFieldContext c) - { - var odataQuery = "?" + - string.Join("&", - c.Arguments - .Select(x => new { x.Key, Value = x.Value.ToString() }).Where(x => !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => $"${x.Key}={x.Value}")); - - return odataQuery; - } - - private static IFieldResolver ResolveAsync(Func> action) - { - return new FuncFieldResolver>(c => - { - var e = (GraphQLExecutionContext)c.UserContext; - - return action(c, e); - }); - } } } 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 new file mode 100644 index 000000000..1d3964f7e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs @@ -0,0 +1,112 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL; +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Domain.Apps.Entities.Assets; + +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 (GUID).", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullGuid + } + }; + + public static readonly IFieldResolver Resolver = new FuncFieldResolver(c => + { + var id = c.GetArgument("id"); + + return ((GraphQLExecutionContext)c.UserContext).FindAssetAsync(id); + }); + } + + 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 = string.Empty, + ResolvedType = AllTypes.String + }, + new QueryArgument(AllTypes.None) + { + Name = "orderby", + Description = "Optional OData order definition.", + DefaultValue = string.Empty, + 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/AssetGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index 66b3d5728..160777083 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -5,12 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using GraphQL.Resolvers; using GraphQL.Types; -using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { @@ -24,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "id", ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id.ToString()), + Resolver = EntityResolvers.Id, Description = "The id of the asset." }); @@ -32,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "version", ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), + Resolver = EntityResolvers.Version, Description = "The version of the asset." }); @@ -40,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "created", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), + Resolver = EntityResolvers.Created, Description = "The date and time when the asset has been created." }); @@ -48,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "createdBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), + Resolver = EntityResolvers.CreatedBy, Description = "The user that has created the asset." }); @@ -56,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModified", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), + Resolver = EntityResolvers.LastModified, Description = "The date and time when the asset has been modified last." }); @@ -64,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModifiedBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Resolver = EntityResolvers.LastModifiedBy, Description = "The user that has updated the asset last." }); @@ -72,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "mimeType", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.MimeType), + Resolver = AssetResolvers.MimeType, Description = "The mime type." }); @@ -80,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "url", ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetUrl(), + Resolver = AssetResolvers.Url, Description = "The url to the asset." }); @@ -88,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "thumbnailUrl", ResolvedType = AllTypes.String, - Resolver = model.ResolveAssetThumbnailUrl(), + Resolver = AssetResolvers.ThumbnailUrl, Description = "The thumbnail url to the asset." }); @@ -96,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileName", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName), + Resolver = AssetResolvers.FileName, Description = "The file name." }); @@ -104,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileHash", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileHash), + Resolver = AssetResolvers.FileHash, Description = "The hash of the file. Can be null for old files." }); @@ -112,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileType", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.FileName!.FileType()), + Resolver = AssetResolvers.FileType, Description = "The file type." }); @@ -120,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileSize", ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileSize), + Resolver = AssetResolvers.FileSize, Description = "The size of the file in bytes." }); @@ -128,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "fileVersion", ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.FileVersion), + Resolver = AssetResolvers.FileVersion, Description = "The version of the file." }); @@ -136,7 +132,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "slug", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Slug), + Resolver = AssetResolvers.Slug, Description = "The file name as slug." }); @@ -144,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "isProtected", ResolvedType = AllTypes.NonNullBoolean, - Resolver = Resolve(x => x.IsProtected), + Resolver = AssetResolvers.IsProtected, Description = "True, when the asset is not public." }); @@ -152,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "isImage", ResolvedType = AllTypes.NonNullBoolean, - Resolver = Resolve(x => x.Type == AssetType.Image), + Resolver = AssetResolvers.IsImage, Description = "Determines if the uploaded file is an image.", DeprecationReason = "Use 'type' field instead." }); @@ -161,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "pixelWidth", ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.Metadata.GetPixelWidth()), + Resolver = AssetResolvers.PixelWidth, Description = "The width of the image in pixels if the asset is an image.", DeprecationReason = "Use 'metadata' field instead." }); @@ -170,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "pixelHeight", ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.Metadata.GetPixelHeight()), + Resolver = AssetResolvers.PixelHeight, Description = "The height of the image in pixels if the asset is an image.", DeprecationReason = "Use 'metadata' field instead." }); @@ -179,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "type", ResolvedType = AllTypes.NonNullAssetType, - Resolver = Resolve(x => x.Type), + Resolver = AssetResolvers.Type, Description = "The type of the image." }); @@ -187,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "metadataText", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.MetadataText), + Resolver = AssetResolvers.MetadataText, Description = "The text representation of the metadata." }); @@ -195,7 +191,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "tags", ResolvedType = null, - Resolver = Resolve(x => x.TagNames), + Resolver = AssetResolvers.Tags, Description = "The asset tags.", Type = AllTypes.NonNullTagsType }); @@ -203,9 +199,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "metadata", - Arguments = AllTypes.PathArguments, + Arguments = AssetActions.Metadata.Arguments, ResolvedType = AllTypes.NoopJson, - Resolver = ResolveMetadata(), + Resolver = AssetActions.Metadata.Resolver, Description = "The asset metadata." }); @@ -215,29 +211,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "sourceUrl", ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveAssetSourceUrl(), + Resolver = AssetResolvers.SourceUrl, Description = "The source url of the asset." }); } Description = "An asset"; } - - private static IFieldResolver ResolveMetadata() - { - return new FuncFieldResolver(c => - { - var path = c.Arguments.GetOrDefault(AllTypes.PathName); - - c.Source.Metadata.TryGetByPath(path as string, out var result); - - return result; - }); - } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } } } 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 new file mode 100644 index 000000000..70ee6ede0 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs @@ -0,0 +1,66 @@ +// ========================================================================== +// 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.Id); + }); + + public static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) => + { + return context.UrlGenerator.AssetSource(asset.Id, asset.FileVersion); + }); + + public static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) => + { + return context.UrlGenerator.AssetThumbnail(asset.Id, 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/AssetsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs index 04671da20..458da1624 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs @@ -5,8 +5,6 @@ // 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; @@ -23,24 +21,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "total", ResolvedType = AllTypes.Int, - Resolver = Resolve(x => x.Total), + Resolver = AssetResolvers.ListTotal, Description = "The total count of assets." }); AddField(new FieldType { Name = "items", - Resolver = Resolve(x => x), + Resolver = AssetResolvers.ListItems, ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), Description = "The assets." }); Description = "List of assets and total count of assets."; } - - private static IFieldResolver Resolve(Func, object> action) - { - return new FuncFieldResolver, object>(c => action(c.Source)); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs new file mode 100644 index 000000000..8b93e9e91 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs @@ -0,0 +1,336 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GraphQL; +using GraphQL.Resolvers; +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.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.Validation; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public static class ContentActions + { + public static class Json + { + 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 ValueResolver Resolver = new ValueResolver((value, c) => + { + if (c.Arguments.TryGetValue("path", out var p) && p is string path) + { + value.TryGetByPath(path, out var result); + + return result!; + } + + return value; + }); + } + + public static readonly QueryArguments JsonPath = new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "path", + Description = "The path to the json value", + DefaultValue = null, + ResolvedType = AllTypes.String + } + }; + + public static class Find + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The id of the content (GUID).", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullGuid + } + }; + + public static readonly IFieldResolver Resolver = new FuncFieldResolver(c => + { + var id = c.GetArgument("id"); + + return ((GraphQLExecutionContext)c.UserContext).FindContentAsync(id); + }); + } + + public static class Query + { + private static QueryArguments? arguments; + + public static QueryArguments Arguments(int pageSize) + { + return arguments ??= new QueryArguments + { + 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 = string.Empty, + ResolvedType = AllTypes.String + }, + new QueryArgument(AllTypes.None) + { + Name = "orderby", + Description = "Optional OData order definition.", + DefaultValue = string.Empty, + ResolvedType = AllTypes.String + }, + new QueryArgument(AllTypes.None) + { + Name = "search", + Description = "Optional OData full text search.", + DefaultValue = string.Empty, + ResolvedType = AllTypes.String + }, + }; + } + + public static IFieldResolver Resolver(Guid schemaId) + { + var schemaIdValue = schemaId.ToString(); + + return new FuncFieldResolver(c => + { + var query = c.BuildODataQuery(); + + return ((GraphQLExecutionContext)c.UserContext).QueryContentsAsync(schemaIdValue, query); + }); + } + } + + public static class Create + { + public static QueryArguments Arguments(IGraphType inputType) + { + return new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "data", + Description = "The data for the content.", + DefaultValue = null, + ResolvedType = new NonNullGraphType(inputType), + }, + new QueryArgument(AllTypes.None) + { + Name = "publish", + Description = "Set to true to autopublish content.", + DefaultValue = false, + ResolvedType = AllTypes.Boolean + } + }; + } + + public static IFieldResolver Resolver(NamedId schemaId) + { + return ResolveAsync(c => + { + var contentPublish = c.GetArgument("publish"); + var contentData = GetContentData(c); + + return new CreateContent { SchemaId = schemaId, Data = contentData, Publish = contentPublish }; + }); + } + } + + public static class UpdateOrPatch + { + public static QueryArguments Arguments(IGraphType inputType) + { + return new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The id of the content (GUID)", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullGuid + }, + new QueryArgument(AllTypes.None) + { + Name = "data", + Description = "The data for the content.", + DefaultValue = null, + ResolvedType = new NonNullGraphType(inputType), + }, + new QueryArgument(AllTypes.None) + { + Name = "expectedVersion", + Description = "The expected version", + DefaultValue = EtagVersion.Any, + ResolvedType = AllTypes.Int + } + }; + } + + public static readonly IFieldResolver Update = ResolveAsync(c => + { + var contentId = c.GetArgument("id"); + var contentData = GetContentData(c); + + return new UpdateContent { ContentId = contentId, Data = contentData }; + }); + + public static readonly IFieldResolver Patch = ResolveAsync(c => + { + var contentId = c.GetArgument("id"); + var contentData = GetContentData(c); + + return new PatchContent { ContentId = contentId, Data = contentData }; + }); + } + + public static class ChangeStatus + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The id of the content (GUID)", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullGuid + }, + new QueryArgument(AllTypes.None) + { + Name = "status", + Description = "The new status", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullString + }, + new QueryArgument(AllTypes.None) + { + Name = "dueTime", + Description = "When to change the status", + DefaultValue = EtagVersion.Any, + ResolvedType = AllTypes.Date + }, + new QueryArgument(AllTypes.None) + { + Name = "expectedVersion", + Description = "The expected version", + DefaultValue = EtagVersion.Any, + ResolvedType = AllTypes.Int + } + }; + + public static readonly IFieldResolver Resolver = ResolveAsync(c => + { + var contentId = c.GetArgument("id"); + var contentStatus = new Status(c.GetArgument("status")); + var contentDueTime = c.GetArgument("dueTime"); + + return new ChangeContentStatus { ContentId = contentId, Status = contentStatus, DueTime = contentDueTime }; + }); + } + + public static class Delete + { + public static readonly QueryArguments Arguments = new QueryArguments + { + new QueryArgument(AllTypes.None) + { + Name = "id", + Description = "The id of the content (GUID)", + DefaultValue = string.Empty, + ResolvedType = AllTypes.NonNullGuid + }, + new QueryArgument(AllTypes.None) + { + Name = "expectedVersion", + Description = "The expected version", + DefaultValue = EtagVersion.Any, + ResolvedType = AllTypes.Int + } + }; + + public static readonly IFieldResolver Resolver = ResolveAsync(c => + { + var contentId = c.GetArgument("id"); + + return new DeleteContent { ContentId = contentId }; + }); + } + + private static NamedContentData GetContentData(IResolveFieldContext c) + { + var source = c.GetArgument>("data"); + + return source.ToNamedContentData((IComplexGraphType)c.FieldDefinition.Arguments.Find("data").Flatten()); + } + + private static IFieldResolver ResolveAsync(Func action) + { + return new FuncFieldResolver>(async c => + { + var e = (GraphQLExecutionContext)c.UserContext; + + try + { + var command = action(c); + + command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any); + + var commandContext = await e.CommandBus.PublishAsync(command); + + return commandContext.Result(); + } + catch (ValidationException ex) + { + c.Errors.Add(new ExecutionError(ex.Message)); + + throw; + } + catch (DomainException ex) + { + c.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/ContentDataFlatGraphType.cs index 14314ca5e..6fdac6a17 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; @@ -28,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = fieldName, Arguments = args, - Resolver = PartitionResolver(valueResolver, field.Name), + Resolver = ContentResolvers.FlatPartition(valueResolver, field.Name), ResolvedType = resolvedType, Description = field.RawProperties.Hints }); @@ -37,22 +36,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The structure of the flat {schemaName} data type."; } - - private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) - { - return new FuncFieldResolver(c => - { - var source = (FlatContentData)c.Source; - - if (source.TryGetValue(key, out var value) && value != null) - { - return valueResolver(value, c); - } - else - { - return null; - } - }); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index 04dfc7c2e..f5f435315 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -5,14 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; -using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { @@ -24,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields()) { - var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, fieldName); + var (resolvedType, valueResolver, args) = model.GetGraphType(schema, field, typeName); if (valueResolver != null) { @@ -43,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = partitionKey.EscapePartition(), Arguments = args, - Resolver = PartitionResolver(valueResolver, partitionKey), + Resolver = ContentResolvers.Partition(valueResolver, partitionKey), ResolvedType = resolvedType, Description = field.RawProperties.Hints }); @@ -54,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = fieldName, - Resolver = FieldResolver(field), + Resolver = ContentResolvers.Field(field), ResolvedType = fieldGraphType, Description = $"The {displayName} field." }); @@ -63,30 +59,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The structure of the {schemaName} data type."; } - - private static FuncFieldResolver PartitionResolver(ValueResolver valueResolver, string key) - { - return new FuncFieldResolver(c => - { - var source = (ContentFieldData)c.Source; - - if (source.TryGetValue(key, out var value) && value != null) - { - return valueResolver(value, c); - } - else - { - return null; - } - }); - } - - private static FuncFieldResolver?> FieldResolver(RootField field) - { - return new FuncFieldResolver?>(c => - { - return c.Source?.GetOrDefault(field.Name); - }); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs new file mode 100644 index 000000000..b07492807 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class ContentDataInputGraphType : InputObjectGraphType + { + public ContentDataInputGraphType(ISchemaEntity schema, string schemaName, string schemaType, IGraphModel model) + { + Name = $"{schemaType}DataInputDto"; + + foreach (var (field, fieldName, typeName) in schema.SchemaDef.Fields.SafeFields().Where(x => x.Field.IsForApi(true))) + { + var resolvedType = model.GetInputGraphType(schema, field, typeName); + + if (resolvedType != null) + { + var displayName = field.DisplayName(); + + var fieldGraphType = new InputObjectGraphType + { + Name = $"{schemaType}Data{typeName}InputDto" + }; + + var partitioning = model.ResolvePartition(field.Partitioning); + + foreach (var partitionKey in partitioning.AllKeys) + { + fieldGraphType.AddField(new FieldType + { + Name = partitionKey.EscapePartition(), + Resolver = null, + ResolvedType = resolvedType, + Description = field.RawProperties.Hints + }).WithSourceName( partitionKey); + } + + fieldGraphType.Description = $"The structure of the {displayName} field of the {schemaName} content input type."; + + AddField(new FieldType + { + Name = fieldName, + Resolver = null, + ResolvedType = fieldGraphType, + Description = $"The {displayName} field." + }).WithSourceName(field.Name); + } + } + + Description = $"The structure of the {schemaName} data input type."; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index 4305893e2..05ba7fb4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -5,12 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Linq; -using GraphQL.Resolvers; using GraphQL.Types; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.ConvertContent; using Squidex.Domain.Apps.Entities.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types @@ -34,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "id", ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id), + Resolver = EntityResolvers.Id, Description = $"The id of the {schemaName} content." }); @@ -42,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "version", ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), + Resolver = EntityResolvers.Version, Description = $"The version of the {schemaName} content." }); @@ -50,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "created", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), + Resolver = EntityResolvers.Created, Description = $"The date and time when the {schemaName} content has been created." }); @@ -58,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "createdBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), + Resolver = EntityResolvers.CreatedBy, Description = $"The user that has created the {schemaName} content." }); @@ -66,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModified", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), + Resolver = EntityResolvers.LastModified, Description = $"The date and time when the {schemaName} content has been modified last." }); @@ -74,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModifiedBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Resolver = EntityResolvers.LastModifiedBy, Description = $"The user that has updated the {schemaName} content last." }); @@ -82,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "status", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), + Resolver = ContentResolvers.Status, Description = $"The the status of the {schemaName} content." }); @@ -90,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "statusColor", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.StatusColor), + Resolver = ContentResolvers.StatusColor, Description = $"The color status of the {schemaName} content." }); @@ -112,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "url", ResolvedType = AllTypes.NonNullString, - Resolver = model.ResolveContentUrl(schema), + Resolver = ContentResolvers.Url, Description = $"The url to the the {schemaName} content." }); @@ -124,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "data", ResolvedType = new NonNullGraphType(contentDataType), - Resolver = Resolve(x => x.Data), + Resolver = ContentResolvers.Data, Description = $"The data of the {schemaName} content." }); } @@ -137,25 +133,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "flatData", ResolvedType = new NonNullGraphType(contentDataTypeFlat), - Resolver = ResolveFlat(x => x.Data), + Resolver = ContentResolvers.FlatData, Description = $"The flat data of the {schemaName} content." }); } } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } - - private static IFieldResolver ResolveFlat(Func action) - { - return new FuncFieldResolver(c => - { - var context = (GraphQLExecutionContext)c.UserContext; - - return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master); - }); - } } } 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 index 32e0657b5..9afae5c5b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs @@ -5,13 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using GraphQL.Resolvers; using GraphQL.Types; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public sealed class ContentInterfaceGraphType : InterfaceGraphType + public sealed class ContentInterfaceGraphType : InterfaceGraphType { public ContentInterfaceGraphType() { @@ -21,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "id", ResolvedType = AllTypes.NonNullGuid, - Resolver = Resolve(x => x.Id), + Resolver = EntityResolvers.Id, Description = "The id of the content." }); @@ -29,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "version", ResolvedType = AllTypes.NonNullInt, - Resolver = Resolve(x => x.Version), + Resolver = EntityResolvers.Version, Description = "The version of the content." }); @@ -37,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "created", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.Created), + Resolver = EntityResolvers.Created, Description = "The date and time when the content has been created." }); @@ -45,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "createdBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.CreatedBy.ToString()), + Resolver = EntityResolvers.CreatedBy, Description = "The user that has created the content." }); @@ -53,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModified", ResolvedType = AllTypes.NonNullDate, - Resolver = Resolve(x => x.LastModified), + Resolver = EntityResolvers.LastModified, Description = "The date and time when the content has been modified last." }); @@ -61,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "lastModifiedBy", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.LastModifiedBy.ToString()), + Resolver = EntityResolvers.LastModifiedBy, Description = "The user that has updated the content last." }); @@ -69,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "status", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()), + Resolver = ContentResolvers.Status, Description = "The the status of the content." }); @@ -77,16 +75,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "statusColor", ResolvedType = AllTypes.NonNullString, - Resolver = Resolve(x => x.StatusColor), + Resolver = ContentResolvers.StatusColor, Description = "The color status of the content." }); Description = "The structure of all content types."; } - - private static IFieldResolver Resolve(Func action) - { - return new FuncFieldResolver(c => action(c.Source)); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs new file mode 100644 index 000000000..9bb78100b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs @@ -0,0 +1,106 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using GraphQL; +using GraphQL.Resolvers; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.ConvertContent; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public static class ContentResolvers + { + public static IFieldResolver NestedValue(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + if (c.Source.TryGetValue(key, out var value)) + { + return valueResolver(value, c); + } + + return null; + }); + } + + public static IFieldResolver Partition(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + if (c.Source.TryGetValue(key, out var value) && value != null) + { + return valueResolver(value, c); + } + + return null; + }); + } + + public static IFieldResolver FlatPartition(ValueResolver valueResolver, string key) + { + return new FuncFieldResolver(c => + { + if (c.Source.TryGetValue(key, out var value) && value != null) + { + return valueResolver(value, c); + } + + return null; + }); + } + + public static IFieldResolver Field(RootField field) + { + var fieldName = field.Name; + + return new FuncFieldResolver?>(c => + { + return c.Source?.GetOrDefault(fieldName); + }); + } + + public static readonly IFieldResolver Url = Resolve((content, _, context) => + { + var appId = content.AppId; + + return context.UrlGenerator.ContentUI(appId, content.SchemaId, content.Id); + }); + + public static readonly IFieldResolver FlatData = Resolve((content, c, context) => + { + var language = context.Context.App.LanguagesConfig.Master; + + return content.Data.ToFlatten(language); + }); + + 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) + { + 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/ContentsResultGraphType.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs index d7ae791e0..1d01f9959 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs @@ -5,8 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; -using GraphQL.Resolvers; using GraphQL.Types; using Squidex.Infrastructure; @@ -21,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "total", - Resolver = Resolver(x => x.Total), + Resolver = ContentResolvers.ListTotal, ResolvedType = AllTypes.NonNullInt, Description = $"The total number of {schemaName} items." }); @@ -29,17 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types AddField(new FieldType { Name = "items", - Resolver = Resolver(x => x), + Resolver = ContentResolvers.ListItems, ResolvedType = new ListGraphType(new NonNullGraphType(contentType)), Description = $"The {schemaName} items." }); Description = $"List of {schemaName} items and total count."; } - - private static IFieldResolver Resolver(Func, object> action) - { - return new FuncFieldResolver, object>(c => action(c.Source)); - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs new file mode 100644 index 000000000..287ec4f20 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using GraphQL.Resolvers; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public static class EntityResolvers + { + public static readonly IFieldResolver Id = Resolve(x => x.Id.ToString()); + public static readonly IFieldResolver Created = Resolve(x => x.Created); + public static readonly IFieldResolver CreatedBy = Resolve(x => x.CreatedBy.ToString()); + public static readonly IFieldResolver LastModified = Resolve(x => x.LastModified.ToString()); + 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) + { + return new FuncFieldResolver(c => action(c.Source)); + } + } +} 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 a36e6ec6f..78bf139cc 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 @@ -7,7 +7,10 @@ using System.Collections.Generic; using System.Linq; +using GraphQL; +using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; using Squidex.Text; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types @@ -27,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types private static IEnumerable<(T Field, string Name, string Type)> FieldNames(this IEnumerable fields) where T : IField { - return fields.ForApi().Select(field => (field, field.Name.ToCamelCase(), field.TypeName())); + return fields.ForApi(true).Select(field => (field, CasingExtensions.ToCamelCase(field.Name), field.TypeName())); } private static string SafeString(this string value, int index) @@ -44,5 +47,43 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return value; } + + 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; + } + + public static FieldType WithSourceName(this FieldType field, object value) + { + field.Metadata["sourceName"] = value; + + return field; + } + + public static string GetSourceName(this FieldType field) + { + return field.Metadata.GetOrAddDefault("sourceName") as string ?? field.Name; + } + + public static IGraphType Flatten(this QueryArgument type) + { + return type.ResolvedType.Flatten(); + } + + public static IGraphType Flatten(this IGraphType type) + { + if (type is IProvideResolvedType provider) + { + return provider.ResolvedType.Flatten(); + } + + return type; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs new file mode 100644 index 000000000..815ce052b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs @@ -0,0 +1,85 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class InputFieldVisitor : IFieldVisitor + { + private readonly ISchemaEntity schema; + private readonly IGraphModel model; + private readonly string fieldName; + + public InputFieldVisitor(ISchemaEntity schema, IGraphModel model, string fieldName) + { + this.model = model; + this.schema = schema; + this.fieldName = fieldName; + } + + public IGraphType? Visit(IArrayField field) + { + var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedInputGraphType(model, schema, field, fieldName))); + + return schemaFieldType; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.References; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Boolean; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Date; + } + + public IGraphType? Visit(IField field) + { + return GeolocationInputGraphType.Nullable; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Json; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Float; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Json; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.String; + } + + public IGraphType? Visit(IField field) + { + return AllTypes.Tags; + } + + public IGraphType? Visit(IField field) + { + return null; + } + } +} 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 b94045f27..b3d28e6f8 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 @@ -5,7 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GraphQL.Resolvers; +using System.Linq; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; @@ -24,20 +24,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Name = $"{schemaType}{fieldName}ChildDto"; - foreach (var (nestedField, nestedName, _) in field.Fields.SafeFields()) + foreach (var (nestedField, nestedName, typeName) in field.Fields.SafeFields().Where(x => x.Field.IsForApi())) { - var (resolveType, valueResolver, args) = model.GetGraphType(schema, nestedField, nestedName); + var (resolvedType, valueResolver, args) = model.GetGraphType(schema, nestedField, typeName); - if (resolveType != null && valueResolver != null) + if (resolvedType != null && valueResolver != null) { - var resolver = ValueResolver(nestedField, valueResolver); + var resolver = ContentResolvers.NestedValue(valueResolver, nestedField.Name); AddField(new FieldType { Name = nestedName, Arguments = args, + ResolvedType = resolvedType, Resolver = resolver, - ResolvedType = resolveType, Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." }); } @@ -45,20 +45,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; } - - private static FuncFieldResolver ValueResolver(NestedField nestedField, ValueResolver resolver) - { - return new FuncFieldResolver(c => - { - if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value)) - { - return resolver(value, c); - } - else - { - return null; - } - }); - } } } 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 new file mode 100644 index 000000000..f6bd5ff2e --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs @@ -0,0 +1,45 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Linq; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class NestedInputGraphType : InputObjectGraphType + { + public NestedInputGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field, string fieldName) + { + var schemaType = schema.TypeName(); + var schemaName = schema.DisplayName(); + + var fieldDisplayName = field.DisplayName(); + + Name = $"{schemaType}{fieldName}InputChildDto"; + + foreach (var (nestedField, nestedName, typeName) in field.Fields.SafeFields().Where(x => x.Field.IsForApi(true))) + { + var resolvedType = model.GetInputGraphType(schema, nestedField, typeName); + + if (resolvedType != null) + { + AddField(new FieldType + { + Name = nestedName, + Resolver = null, + ResolvedType = resolvedType, + Description = $"The {fieldDisplayName}/{nestedField.DisplayName()} nested field." + }).WithSourceName(nestedField.Name); + } + } + + Description = $"The structure of the {schemaName}.{fieldDisplayName} nested schema."; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 3efa4bdaa..496432f00 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Linq; +using GraphQL; using GraphQL.Types; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas; @@ -16,7 +17,7 @@ using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public delegate object ValueResolver(IJsonValue value, ResolveFieldContext context); + public delegate object ValueResolver(IJsonValue value, IResolveFieldContext context); public sealed class QueryGraphTypeVisitor : IFieldVisitor<(IGraphType?, ValueResolver?, QueryArguments?)> { @@ -42,7 +43,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IArrayField field) { - return ResolveNested(field); + var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); + + return (schemaFieldType, NoopResolver, null); } public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) @@ -90,30 +93,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return (null, null, null); } - private static (IGraphType?, ValueResolver?, QueryArguments?) ResolveDefault(IGraphType type) + public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) { - return (type, NoopResolver, null); + return (AllTypes.NoopJson, ContentActions.Json.Resolver, ContentActions.Json.Arguments); } - private (IGraphType?, ValueResolver?, QueryArguments?) ResolveNested(IArrayField field) - { - var schemaFieldType = new ListGraphType(new NonNullGraphType(new NestedGraphType(model, schema, field, fieldName))); - - return (schemaFieldType, NoopResolver, null); - } - - public (IGraphType?, ValueResolver?, QueryArguments?) Visit(IField field) + private static (IGraphType?, ValueResolver?, QueryArguments?) ResolveDefault(IGraphType type) { - var resolver = new ValueResolver((value, c) => - { - var path = c.Arguments.GetOrDefault(AllTypes.PathName); - - value.TryGetByPath(path as string, out var result); - - return result!; - }); - - return (AllTypes.NoopJson, resolver, AllTypes.PathArguments); + return (type, NoopResolver, null); } private (IGraphType?, ValueResolver?, QueryArguments?) ResolveAssets() 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/Utils/Converters.cs new file mode 100644 index 000000000..2c8e5077b --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using GraphQL.Types; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure.Json.Objects; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public static class Converters + { + public static NamedContentData ToNamedContentData(this IDictionary source, IComplexGraphType type) + { + var result = new NamedContentData(); + + foreach (var field in type.Fields) + { + if (source.TryGetValue(field.Name, out var t) && t is IDictionary nested && field.ResolvedType is IComplexGraphType complexType) + { + result[field.GetSourceName()] = nested.ToFieldData(complexType); + } + } + + return result; + } + + public static ContentFieldData ToFieldData(this IDictionary source, IComplexGraphType type) + { + var result = new ContentFieldData(); + + foreach (var field in type.Fields) + { + if (source.TryGetValue(field.Name, out var value)) + { + if (value is List list && field.ResolvedType.Flatten() is IComplexGraphType nestedType) + { + var arr = new JsonArray(); + + foreach (var item in list) + { + if (item is IDictionary nested) + { + arr.Add(nested.ToNestedData(nestedType)); + } + } + + result[field.GetSourceName()] = arr; + } + else + { + result[field.GetSourceName()] = JsonConverter.ParseJson(value); + } + } + } + + return result; + } + + public static IJsonValue ToNestedData(this IDictionary source, IComplexGraphType type) + { + var result = JsonValue.Object(); + + foreach (var field in type.Fields) + { + if (source.TryGetValue(field.Name, out var value)) + { + result[field.GetSourceName()] = JsonConverter.ParseJson(value); + } + } + + return result; + } + } +} 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/Utils/EntitySavedGraphType.cs new file mode 100644 index 000000000..5d01f83bd --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Resolvers; +using GraphQL.Types; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types +{ + public sealed class EntitySavedGraphType : ObjectGraphType + { + public static readonly IGraphType Nullable = new EntitySavedGraphType(); + + public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); + + private EntitySavedGraphType() + { + Name = "EntitySavedResultDto"; + + AddField(new FieldType + { + Name = "version", + Resolver = ResolveVersion(), + ResolvedType = AllTypes.NonNullLong, + Description = "The new version of the item." + }); + + Description = "The result of a mutation"; + } + + private static IFieldResolver ResolveVersion() + { + return new FuncFieldResolver(x => + { + return x.Source.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/Utils/GeolocationInputGraphType.cs new file mode 100644 index 000000000..62054a074 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs @@ -0,0 +1,35 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Types; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class GeolocationInputGraphType : InputObjectGraphType + { + public static readonly IGraphType Nullable = new GeolocationInputGraphType(); + + public static readonly IGraphType NonNull = new NonNullGraphType(Nullable); + + private GeolocationInputGraphType() + { + Name = "GeolocationInputDto"; + + AddField(new FieldType + { + Name = "latitude", + ResolvedType = AllTypes.NonNullFloat + }); + + AddField(new FieldType + { + Name = "longitude", + ResolvedType = AllTypes.NonNullFloat + }); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs deleted file mode 100644 index a19703dbd..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using GraphQL.Language.AST; -using GraphQL.Types; - -namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils -{ - public sealed class GuidGraphType2 : ScalarGraphType - { - public GuidGraphType2() - { - Name = "Guid"; - - Description = "The `Guid` scalar type global unique identifier"; - } - - public override object? Serialize(object value) - { - return ParseValue(value)?.ToString(); - } - - public override object? ParseValue(object value) - { - if (value is Guid guid) - { - return guid; - } - - var inputValue = value?.ToString()?.Trim('"'); - - if (Guid.TryParse(inputValue, out guid)) - { - return guid; - } - - return null; - } - - public override object? ParseLiteral(IValue value) - { - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; - } - } -} 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/Utils/InstantConverter.cs new file mode 100644 index 000000000..4c3e346df --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using GraphQL.Language.AST; +using GraphQL.Types; +using NodaTime; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils +{ + public sealed class InstantConverter : IAstFromValueConverter + { + public static readonly InstantConverter Instance = new InstantConverter(); + + private InstantConverter() + { + } + + public IValue Convert(object value, IGraphType type) + { + return new InstantValueNode((Instant)value); + } + + public bool Matches(object value, IGraphType type) + { + return type is InstantGraphType; + } + } +} 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/Utils/InstantGraphType.cs index 21ac16ccd..2ba699892 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs @@ -25,17 +25,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils public override object? ParseLiteral(IValue value) { - if (value is InstantValue timeValue) + switch (value) { - return ParseValue(timeValue.Value); + case InstantValueNode timeValue: + return timeValue.Value; + case StringValue stringValue: + return ParseValue(stringValue.Value); + default: + return null; } - - if (value is StringValue stringValue) - { - return ParseValue(stringValue.Value); - } - - return null; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs similarity index 80% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs index 1af6d2e92..b623c39a3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs @@ -10,16 +10,16 @@ using NodaTime; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils { - public sealed class InstantValue : ValueNode + public sealed class InstantValueNode : ValueNode { - public InstantValue(Instant value) + public InstantValueNode(Instant value) { Value = value; } protected override bool Equals(ValueNode node) { - return Value.Equals(node.Value); + return Equals(Value, node.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/Utils/JsonConverter.cs index ea597a4b3..c11397608 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Collections.Generic; using GraphQL.Language.AST; using GraphQL.Types; using Squidex.Infrastructure.Json.Objects; @@ -21,12 +22,51 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils public IValue Convert(object value, IGraphType type) { - return new JsonValueNode(value as JsonObject ?? JsonValue.Null); + return new JsonValueNode(ParseJson(value)); } public bool Matches(object value, IGraphType type) { - return value is JsonObject; + return type is JsonGraphType; + } + + public static IJsonValue ParseJson(object value) + { + switch (value) + { + case ListValue listValue: + return ParseJson(listValue.Value); + + case ObjectValue objectValue: + return ParseJson(objectValue.Value); + + case Dictionary dictionary: + { + var json = JsonValue.Object(); + + foreach (var (key, inner) in dictionary) + { + json[key] = ParseJson(inner); + } + + return json; + } + + case List list: + { + var array = JsonValue.Array(); + + foreach (var item in list) + { + array.Add(ParseJson(item)); + } + + return array; + } + + default: + return JsonValue.Create(value); + } } } } 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/Utils/JsonGraphType.cs index 5788d9412..c59a013c2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils public override object ParseValue(object value) { - return value; + return JsonConverter.ParseJson(value); } public override object ParseLiteral(IValue value) 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/Utils/JsonValueNode.cs index 7ac033df6..7b784ea65 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils protected override bool Equals(ValueNode node) { - return false; + return Equals(Value, node.Value); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs index 5e00eaf6a..1491334bb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs @@ -15,9 +15,9 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Queries { - public class QueryExecutionContext + public class QueryExecutionContext : Dictionary { - private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); private readonly IContentQueryService contentQuery; private readonly IAssetQueryService assetQuery; @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return asset; } - public virtual async Task FindContentAsync(Guid schemaId, Guid id) + public virtual async Task FindContentAsync(Guid schemaId, Guid id) { var content = cachedContents.GetOrDefault(id); @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return content; } - public virtual async Task> QueryAssetsAsync(string query) + public virtual async Task> QueryAssetsAsync(string query) { var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithODataQuery(query)); @@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return assets; } - public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) + public virtual async Task> QueryContentsAsync(string schemaIdOrName, string query) { var result = await contentQuery.QueryAsync(context, schemaIdOrName, Q.Empty.WithODataQuery(query)); @@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return ids.Select(cachedAssets.GetOrDefault).NotNull().ToList(); } - public virtual async Task> GetReferencedContentsAsync(ICollection ids) + public virtual async Task> GetReferencedContentsAsync(ICollection ids) { Guard.NotNull(ids, nameof(ids)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index e8dc8cf01..ca915e201 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -24,7 +24,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 4014cee09..fde1d0a57 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.EventSourcing diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs index 7a0de20a7..45644abae 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using GraphQL; +using GraphQL.NewtonsoftJson; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Reflection; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs index c2a4e89da..79156fe43 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using GraphQL; +using GraphQL.NewtonsoftJson; using Newtonsoft.Json.Linq; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Infrastructure.Reflection; diff --git a/backend/src/Squidex/Config/Domain/QueryServices.cs b/backend/src/Squidex/Config/Domain/QueryServices.cs index 278fd7b4a..183513e18 100644 --- a/backend/src/Squidex/Config/Domain/QueryServices.cs +++ b/backend/src/Squidex/Config/Domain/QueryServices.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using GraphQL; using GraphQL.DataLoader; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -30,9 +29,6 @@ namespace Squidex.Config.Domain exposeSourceUrl)) .As(); - services.AddSingletonAs(x => new FuncDependencyResolver(x.GetRequiredService)) - .As(); - services.AddSingletonAs() .As(); diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index c0426a907..6ec90ec91 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -35,6 +35,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs index 7461934c8..19fed832a 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using FluentAssertions; -using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Xunit; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs new file mode 100644 index 000000000..4b8776d93 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLIntrospectionTests : GraphQLTestBase + { + [Fact] + public async Task Should_introspect() + { + const string query = @" + query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + args { + ...InputValue + } + onOperation + onFragment + onField + } + } + } + + fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + }"; + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, OperationName = "IntrospectionQuery" }); + + var json = serializer.Serialize(result.Response, true); + + Assert.NotEmpty(json); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs new file mode 100644 index 000000000..ba6f06897 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -0,0 +1,319 @@ +// ========================================================================== +// Squidex Headless CMS +// ================================ ========================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using FakeItEasy; +using GraphQL; +using GraphQL.NewtonsoftJson; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.GraphQL +{ + public class GraphQLMutationTests : GraphQLTestBase + { + private readonly Guid contentId = Guid.NewGuid(); + private readonly IEnrichedContentEntity content; + private readonly CommandContext commandContext = new CommandContext(new PatchContent(), A.Dummy()); + + public GraphQLMutationTests() + { + content = TestContent.Create(schemaId, contentId, schemaRefId1.Id, schemaRefId2.Id, null); + + A.CallTo(() => commandBus.PublishAsync(A.Ignored)) + .Returns(commandContext); + } + + [Fact] + public async Task Should_return_single_content_when_creating_content() + { + var query = @" + mutation { + createMySchemaContent(data: ) { + + } + }".Replace("", GetDataString()).Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + createMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.SchemaId.Equals(schemaId) && + x.ExpectedVersion == EtagVersion.Any && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_when_creating_content_with_variable() + { + var query = @" + mutation OP($data: MySchemaDataInputDto!) { + createMySchemaContent(data: $data) { + + } + }".Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() }); + + var expected = new + { + data = new + { + createMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.SchemaId.Equals(schemaId) && + x.ExpectedVersion == EtagVersion.Any && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_when_updating_content() + { + var query = @" + mutation { + updateMySchemaContent(id: """", data: , expectedVersion: 10) { + + } + }".Replace("", contentId.ToString()).Replace("", GetDataString()).Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + updateMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_when_updating_content_with_variable() + { + var query = @" + mutation OP($data: MySchemaDataInputDto!) { + updateMySchemaContent(id: """", data: $data, expectedVersion: 10) { + + } + }".Replace("", contentId.ToString()).Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() }); + + var expected = new + { + data = new + { + updateMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_when_patching_content() + { + var query = @" + mutation { + patchMySchemaContent(id: """", data: , expectedVersion: 10) { + + } + }".Replace("", contentId.ToString()).Replace("", GetDataString()).Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query }); + + var expected = new + { + data = new + { + patchMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_return_single_content_when_patching_content_with_variable() + { + var query = @" + mutation OP($data: MySchemaDataInputDto!) { + patchMySchemaContent(id: """", data: $data, expectedVersion: 10) { + + } + }".Replace("", contentId.ToString()).Replace("", TestContent.AllFields); + + commandContext.Complete(content); + + var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query, Inputs = GetInput() }); + + var expected = new + { + data = new + { + patchMySchemaContent = TestContent.Response(content) + } + }; + + AssertResult(expected, result); + + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.Data.Equals(content.Data)))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_publish_command_for_status_change() + { + var dueTime = SystemClock.Instance.GetCurrentInstant().WithoutMs(); + + var query = @" + mutation { + publishMySchemaContent(id: """", status: ""Published"", dueTime: ""