Browse Source

Refactorings and Mutations (#572)

* Refactorings 

* GraphQL mutations.

* Refactoring and fix for json path arguments.
pull/575/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
a93376f0da
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs
  2. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  3. 39
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  4. 52
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  5. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  6. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Middlewares.cs
  7. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  8. 100
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  9. 187
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  10. 112
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetActions.cs
  11. 71
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  12. 66
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs
  13. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs
  14. 336
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentActions.cs
  15. 20
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataFlatGraphType.cs
  16. 35
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  17. 62
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataInputGraphType.cs
  18. 41
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  19. 25
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentInterfaceGraphType.cs
  20. 106
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResolvers.cs
  21. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs
  22. 27
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/EntityResolvers.cs
  23. 43
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Extensions.cs
  24. 85
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs
  25. 27
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedGraphType.cs
  26. 45
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs
  27. 31
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs
  28. 79
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/Converters.cs
  29. 43
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/EntitySavedGraphType.cs
  30. 35
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GeolocationInputGraphType.cs
  31. 55
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs
  32. 32
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantConverter.cs
  33. 14
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantGraphType.cs
  34. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValueNode.cs
  35. 44
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonConverter.cs
  36. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonGraphType.cs
  37. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/JsonValueNode.cs
  38. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  39. 2
      backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj
  40. 1
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  41. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLGetDto.cs
  42. 1
      backend/src/Squidex/Areas/Api/Controllers/Contents/Models/GraphQLPostDto.cs
  43. 4
      backend/src/Squidex/Config/Domain/QueryServices.cs
  44. 1
      backend/src/Squidex/Squidex.csproj
  45. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs
  46. 104
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLIntrospectionTests.cs
  47. 319
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  48. 702
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  49. 170
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  50. 116
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs
  51. 305
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs
  52. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  53. 64
      backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs
  54. 6
      backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs
  55. 344
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  56. 70
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs
  57. 31
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs
  58. 20
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldExtensions.cs
  59. 71
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs
  60. 47
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/NestedInputGraphType.cs

1
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;

14
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<IAppProvider>().GetSchemasAsync(app.Id);
var allSchemas = await resolver.GetRequiredService<IAppProvider>().GetSchemasAsync(app.Id);
return new GraphQLModel(app,
allSchemas,
GetPageSizeForContents(),
GetPageSizeForAssets(),
resolver.Resolve<IUrlGenerator>());
resolver.GetRequiredService<IUrlGenerator>());
});
}
private int GetPageSizeForContents()
{
return resolver.Resolve<IOptions<ContentOptions>>().Value.DefaultPageSizeGraphQl;
return resolver.GetRequiredService<IOptions<ContentOptions>>().Value.DefaultPageSizeGraphQl;
}
private int GetPageSizeForAssets()
{
return resolver.Resolve<IOptions<AssetOptions>>().Value.DefaultPageSizeGraphQl;
return resolver.GetRequiredService<IOptions<AssetOptions>>().Value.DefaultPageSizeGraphQl;
}
private static object CreateCacheKey(Guid appId, string etag)

39
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<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>();
private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>();
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<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())
resolver.GetRequiredService<IAssetQueryService>(),
resolver.GetRequiredService<IContentQueryService>())
{
UrlGenerator = resolver.Resolve<IUrlGenerator>();
UrlGenerator = resolver.GetRequiredService<IUrlGenerator>();
CommandBus = resolver.GetRequiredService<ICommandBus>();
dataLoaderContextAccessor = resolver.Resolve<IDataLoaderContextAccessor>();
dataLoaderContextAccessor = resolver.GetRequiredService<IDataLoaderContextAccessor>();
this.resolver = resolver;
}
public void Setup(ExecutionOptions execution)
{
var loader = resolver.Resolve<DataLoaderDocumentListener>();
var loader = resolver.GetRequiredService<DataLoaderDocumentListener>();
execution.Listeners.Add(loader);
execution.FieldMiddleware.Use(Middlewares.Logging(resolver.Resolve<ISemanticLog>()));
execution.FieldMiddleware.Use(Middlewares.Logging(resolver.GetRequiredService<ISemanticLog>()));
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<IContentEntity?> FindContentAsync(Guid id)
{
var dataLoader = GetContentsLoader();
return await dataLoader.LoadAsync(id);
return await dataLoader.LoadAsync(id).GetResultAsync();
}
public Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
@ -85,13 +90,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return LoadManyAsync(dataLoader, ids);
}
public Task<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(IJsonValue value)
public Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(IJsonValue value)
{
var ids = ParseIds(value);
if (ids == null)
{
return Task.FromResult<IReadOnlyList<IContentEntity>>(EmptyContents);
return Task.FromResult<IReadOnlyList<IEnrichedContentEntity>>(EmptyContents);
}
var dataLoader = GetContentsLoader();
@ -110,9 +115,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
});
}
private IDataLoader<Guid, IContentEntity> GetContentsLoader()
private IDataLoader<Guid, IEnrichedContentEntity> GetContentsLoader()
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IContentEntity>(nameof(GetContentsLoader),
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IEnrichedContentEntity>(nameof(GetContentsLoader),
async batch =>
{
var result = await GetReferencedContentsAsync(new List<Guid>(batch));
@ -123,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static async Task<IReadOnlyList<T>> LoadManyAsync<TKey, T>(IDataLoader<TKey, T> dataLoader, ICollection<TKey> 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();
}

52
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<IAssetEntity, object>(c =>
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.AssetContent(c.Source.Id);
});
return resolver;
}
public IFieldResolver ResolveAssetSourceUrl()
{
var resolver = new FuncFieldResolver<IAssetEntity, object?>(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<IAssetEntity, object?>(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<IContentEntity, object>(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));

11
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);
}
}

9
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<FieldMiddlewareDelegate, FieldMiddlewareDelegate> Logging(ISemanticLog log)
public static Func<ISchema, FieldMiddlewareDelegate, FieldMiddlewareDelegate> 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<FieldMiddlewareDelegate, FieldMiddlewareDelegate> Errors()
public static Func<ISchema, FieldMiddlewareDelegate, FieldMiddlewareDelegate> Errors()
{
return next =>
return (_, next) =>
{
return async context =>
{

20
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<ListGraphType<NonNullGraphType<StringGraphType>>>);
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<AssetType>();
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
});
}
}

100
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<ISchemaEntity> 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<Guid> 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."
});
}
}
}

187
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<Guid>("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<Guid>("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<T>(Func<ResolveFieldContext, GraphQLExecutionContext, Task<T>> action)
{
return new FuncFieldResolver<Task<T>>(c =>
{
var e = (GraphQLExecutionContext)c.UserContext;
return action(c, e);
});
}
}
}

112
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<IEnrichedAssetEntity, object?>(c =>
{
if (c.Arguments.TryGetValue("path", out var path))
{
c.Source.Metadata.TryGetByPath(path as string, out var result);
return result;
}
return c.Source.Metadata;
});
}
public static class Find
{
public static readonly QueryArguments Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = "The id of the asset (GUID).",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
}
};
public static readonly IFieldResolver Resolver = new FuncFieldResolver<object?>(c =>
{
var id = c.GetArgument<Guid>("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<object?>(c =>
{
var query = c.BuildODataQuery();
return ((GraphQLExecutionContext)c.UserContext).QueryAssetsAsync(query);
});
}
}
}

71
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<IEnrichedAssetEntity, object?>(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<IEnrichedAssetEntity, object?> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source));
}
}
}

66
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<T>(Func<IEnrichedAssetEntity, IResolveFieldContext, GraphQLExecutionContext, T> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext));
}
private static IFieldResolver Resolve<T>(Func<IEnrichedAssetEntity, T> action)
{
return new FuncFieldResolver<IEnrichedAssetEntity, object?>(c => action(c.Source));
}
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedAssetEntity>, T> action)
{
return new FuncFieldResolver<IResultList<IEnrichedAssetEntity>, object?>(c => action(c.Source));
}
}
}

11
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<IResultList<IAssetEntity>, object> action)
{
return new FuncFieldResolver<IResultList<IAssetEntity>, object>(c => action(c.Source));
}
}
}

336
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<object?>(c =>
{
var id = c.GetArgument<Guid>("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<object?>(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<Guid> schemaId)
{
return ResolveAsync<IEnrichedContentEntity>(c =>
{
var contentPublish = c.GetArgument<bool>("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<IEnrichedContentEntity>(c =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
return new UpdateContent { ContentId = contentId, Data = contentData };
});
public static readonly IFieldResolver Patch = ResolveAsync<IEnrichedContentEntity>(c =>
{
var contentId = c.GetArgument<Guid>("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<IEnrichedContentEntity>(c =>
{
var contentId = c.GetArgument<Guid>("id");
var contentStatus = new Status(c.GetArgument<string>("status"));
var contentDueTime = c.GetArgument<Instant?>("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<EntitySavedResult>(c =>
{
var contentId = c.GetArgument<Guid>("id");
return new DeleteContent { ContentId = contentId };
});
}
private static NamedContentData GetContentData(IResolveFieldContext c)
{
var source = c.GetArgument<IDictionary<string, object>>("data");
return source.ToNamedContentData((IComplexGraphType)c.FieldDefinition.Arguments.Find("data").Flatten());
}
private static IFieldResolver ResolveAsync<T>(Func<IResolveFieldContext, SquidexCommand> action)
{
return new FuncFieldResolver<Task<T>>(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<T>();
}
catch (ValidationException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
catch (DomainException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
});
}
}
}

20
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<object?> PartitionResolver(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<object?>(c =>
{
var source = (FlatContentData)c.Source;
if (source.TryGetValue(key, out var value) && value != null)
{
return valueResolver(value, c);
}
else
{
return null;
}
});
}
}
}

35
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<object?> PartitionResolver(ValueResolver valueResolver, string key)
{
return new FuncFieldResolver<object?>(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<NamedContentData, IReadOnlyDictionary<string, IJsonValue>?> FieldResolver(RootField field)
{
return new FuncFieldResolver<NamedContentData, IReadOnlyDictionary<string, IJsonValue>?>(c =>
{
return c.Source?.GetOrDefault(field.Name);
});
}
}
}

62
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.";
}
}
}

41
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<IEnrichedContentEntity, object?> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source));
}
private static IFieldResolver ResolveFlat(Func<IEnrichedContentEntity, NamedContentData?> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, FlatContentData?>(c =>
{
var context = (GraphQLExecutionContext)c.UserContext;
return action(c.Source)?.ToFlatten(context.Context.App.LanguagesConfig.Master);
});
}
}
}

25
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<IContentEntity>
public sealed class ContentInterfaceGraphType : InterfaceGraphType<IEnrichedContentEntity>
{
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<IEnrichedContentEntity, object> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object>(c => action(c.Source));
}
}
}

106
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<JsonObject, object?>(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<ContentFieldData, object?>(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<FlatContentData, object?>(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<NamedContentData, IReadOnlyDictionary<string, IJsonValue>?>(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<T>(Func<IEnrichedContentEntity, IResolveFieldContext, GraphQLExecutionContext, T> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source, c, (GraphQLExecutionContext)c.UserContext));
}
private static IFieldResolver Resolve<T>(Func<IEnrichedContentEntity, T> action)
{
return new FuncFieldResolver<IEnrichedContentEntity, object?>(c => action(c.Source));
}
private static IFieldResolver ResolveList<T>(Func<IResultList<IEnrichedContentEntity>, T> action)
{
return new FuncFieldResolver<IResultList<IEnrichedContentEntity>, object?>(c => action(c.Source));
}
}
}

11
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<IResultList<IContentEntity>, object> action)
{
return new FuncFieldResolver<IResultList<IContentEntity>, object>(c => action(c.Source));
}
}
}

27
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<IEntity>(x => x.Id.ToString());
public static readonly IFieldResolver Created = Resolve<IEntity>(x => x.Created);
public static readonly IFieldResolver CreatedBy = Resolve<IEntityWithCreatedBy>(x => x.CreatedBy.ToString());
public static readonly IFieldResolver LastModified = Resolve<IEntity>(x => x.LastModified.ToString());
public static readonly IFieldResolver LastModifiedBy = Resolve<IEntityWithLastModifiedBy>(x => x.LastModifiedBy.ToString());
public static readonly IFieldResolver Version = Resolve<IEntityWithVersion>(x => x.Version);
private static IFieldResolver Resolve<TSource>(Func<TSource, object> action)
{
return new FuncFieldResolver<TSource, object?>(c => action(c.Source));
}
}
}

43
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<T>(this IEnumerable<T> 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;
}
}
}

85
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<IGraphType?>
{
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<AssetsFieldProperties> field)
{
return AllTypes.References;
}
public IGraphType? Visit(IField<BooleanFieldProperties> field)
{
return AllTypes.Boolean;
}
public IGraphType? Visit(IField<DateTimeFieldProperties> field)
{
return AllTypes.Date;
}
public IGraphType? Visit(IField<GeolocationFieldProperties> field)
{
return GeolocationInputGraphType.Nullable;
}
public IGraphType? Visit(IField<JsonFieldProperties> field)
{
return AllTypes.Json;
}
public IGraphType? Visit(IField<NumberFieldProperties> field)
{
return AllTypes.Float;
}
public IGraphType? Visit(IField<ReferencesFieldProperties> field)
{
return AllTypes.Json;
}
public IGraphType? Visit(IField<StringFieldProperties> field)
{
return AllTypes.String;
}
public IGraphType? Visit(IField<TagsFieldProperties> field)
{
return AllTypes.Tags;
}
public IGraphType? Visit(IField<UIFieldProperties> field)
{
return null;
}
}
}

27
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<object?> ValueResolver(NestedField nestedField, ValueResolver resolver)
{
return new FuncFieldResolver<object?>(c =>
{
if (((JsonObject)c.Source).TryGetValue(nestedField.Name, out var value))
{
return resolver(value, c);
}
else
{
return null;
}
});
}
}
}

45
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.";
}
}
}

31
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<AssetsFieldProperties> 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<JsonFieldProperties> 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<JsonFieldProperties> field)
{
var resolver = new ValueResolver((value, c) =>
private static (IGraphType?, ValueResolver?, QueryArguments?) ResolveDefault(IGraphType type)
{
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()

79
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<string, object> source, IComplexGraphType type)
{
var result = new NamedContentData();
foreach (var field in type.Fields)
{
if (source.TryGetValue(field.Name, out var t) && t is IDictionary<string, object> nested && field.ResolvedType is IComplexGraphType complexType)
{
result[field.GetSourceName()] = nested.ToFieldData(complexType);
}
}
return result;
}
public static ContentFieldData ToFieldData(this IDictionary<string, object> 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<object> list && field.ResolvedType.Flatten() is IComplexGraphType nestedType)
{
var arr = new JsonArray();
foreach (var item in list)
{
if (item is IDictionary<string, object> 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<string, object> 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;
}
}
}

43
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<EntitySavedResult>
{
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<EntitySavedResult, long>(x =>
{
return x.Source.Version;
});
}
}
}

35
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
});
}
}
}

55
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/GuidGraphType2.cs

@ -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;
}
}
}

32
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;
}
}
}

14
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)
{
return ParseValue(timeValue.Value);
}
if (value is StringValue stringValue)
switch (value)
{
case InstantValueNode timeValue:
return timeValue.Value;
case StringValue stringValue:
return ParseValue(stringValue.Value);
}
default:
return null;
}
}
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Utils/InstantValue.cs → 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<Instant>
public sealed class InstantValueNode : ValueNode<Instant>
{
public InstantValue(Instant value)
public InstantValueNode(Instant value)
{
Value = value;
}
protected override bool Equals(ValueNode<Instant> node)
{
return Value.Equals(node.Value);
return Equals(Value, node.Value);
}
}
}

44
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<string, object> dictionary:
{
var json = JsonValue.Object();
foreach (var (key, inner) in dictionary)
{
json[key] = ParseJson(inner);
}
return json;
}
case List<object> list:
{
var array = JsonValue.Array();
foreach (var item in list)
{
array.Add(ParseJson(item));
}
return array;
}
default:
return JsonValue.Create(value);
}
}
}
}

2
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)

2
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<IJsonValue> node)
{
return false;
return Equals(Value, node.Value);
}
}
}

12
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<string, object>
{
private readonly ConcurrentDictionary<Guid, IContentEntity?> cachedContents = new ConcurrentDictionary<Guid, IContentEntity?>();
private readonly ConcurrentDictionary<Guid, IEnrichedContentEntity?> cachedContents = new ConcurrentDictionary<Guid, IEnrichedContentEntity?>();
private readonly ConcurrentDictionary<Guid, IEnrichedAssetEntity?> cachedAssets = new ConcurrentDictionary<Guid, IEnrichedAssetEntity?>();
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<IContentEntity?> FindContentAsync(Guid schemaId, Guid id)
public virtual async Task<IEnrichedContentEntity?> 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<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
public virtual async Task<IResultList<IEnrichedAssetEntity>> 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<IResultList<IContentEntity>> QueryContentsAsync(string schemaIdOrName, string query)
public virtual async Task<IResultList<IEnrichedContentEntity>> 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<IReadOnlyList<IContentEntity>> GetReferencedContentsAsync(ICollection<Guid> ids)
public virtual async Task<IReadOnlyList<IEnrichedContentEntity>> GetReferencedContentsAsync(ICollection<Guid> ids)
{
Guard.NotNull(ids, nameof(ids));

2
backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj

@ -24,7 +24,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="GraphQL" Version="3.0.0" />
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00005" />
<PackageReference Include="Lucene.Net.Queries" Version="4.8.0-beta00005" />

1
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

1
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;

1
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;

4
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<IUrlGenerator>();
services.AddSingletonAs(x => new FuncDependencyResolver(x.GetRequiredService))
.As<IDependencyResolver>();
services.AddSingletonAs<DataLoaderContextAccessor>()
.As<IDataLoaderContextAccessor>();

1
backend/src/Squidex/Squidex.csproj

@ -35,6 +35,7 @@
<ItemGroup>
<PackageReference Include="AspNet.Security.OAuth.GitHub" Version="3.1.1" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="IdentityServer4" Version="3.1.3" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="3.1.3" />

1
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;

104
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);
}
}
}

319
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<ICommandBus>());
public GraphQLMutationTests()
{
content = TestContent.Create(schemaId, contentId, schemaRefId1.Id, schemaRefId2.Id, null);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored))
.Returns(commandContext);
}
[Fact]
public async Task Should_return_single_content_when_creating_content()
{
var query = @"
mutation {
createMySchemaContent(data: <DATA>) {
<FIELDS>
}
}".Replace("<DATA>", GetDataString()).Replace("<FIELDS>", 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<CreateContent>.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) {
<FIELDS>
}
}".Replace("<FIELDS>", 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<CreateContent>.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: ""<ID>"", data: <DATA>, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<DATA>", GetDataString()).Replace("<FIELDS>", 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<UpdateContent>.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: ""<ID>"", data: $data, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<FIELDS>", 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<UpdateContent>.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: ""<ID>"", data: <DATA>, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<DATA>", GetDataString()).Replace("<FIELDS>", 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<PatchContent>.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: ""<ID>"", data: $data, expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<FIELDS>", 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<PatchContent>.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: ""<ID>"", status: ""Published"", dueTime: ""<TIME>"", expectedVersion: 10) {
<FIELDS>
}
}".Replace("<ID>", contentId.ToString()).Replace("<TIME>", dueTime.ToString()).Replace("<FIELDS>", TestContent.AllFields);
commandContext.Complete(content);
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
publishMySchemaContent = TestContent.Response(content)
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<ChangeContentStatus>.That.Matches(x =>
x.ContentId == contentId &&
x.DueTime == dueTime &&
x.ExpectedVersion == 10 &&
x.Status == Status.Published)))
.MustHaveHappened();
}
[Fact]
public async Task Should_publish_command_for_delete()
{
var query = @"
mutation {
deleteMySchemaContent(id: ""<ID>"", expectedVersion: 10) {
version
}
}".Replace("<ID>", contentId.ToString());
commandContext.Complete(new EntitySavedResult(13));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
deleteMySchemaContent = new
{
version = 13
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<DeleteContent>.That.Matches(x =>
x.ContentId == contentId &&
x.ExpectedVersion == 10)))
.MustHaveHappened();
}
private Inputs GetInput()
{
var input = new
{
data = TestContent.Data(content, schemaRefId1.Id, schemaRefId2.Id)
};
return JObject.FromObject(input).ToInputs();
}
private string GetDataString()
{
var data = TestContent.Data(content, schemaRefId1.Id, schemaRefId2.Id);
var json = JsonConvert.SerializeObject(data);
return Regex.Replace(json, "\"([^\"]+)\":", x => x.Groups[1].Value + ":");
}
}
}

702
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -19,95 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public class GraphQLQueriesTests : 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);
}
[Theory]
[InlineData(null)]
[InlineData("")]
@ -129,38 +40,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[Fact]
public async Task Should_return_multiple_assets_when_querying_assets()
{
const string query = @"
var query = @"
query {
queryAssets(filter: ""my-query"", top: 30, skip: 5) {
id
version
created
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
isProtected
pixelWidth
pixelHeight
type
metadataText
metadataPixelWidth: metadata(path: ""pixelWidth"")
metadataUnknown: metadata(path: ""unknown"")
metadata
tags
slug
<FIELDS>
}
}";
}".Replace("<FIELDS>", TestAsset.AllFields);
var asset = CreateAsset(Guid.NewGuid());
var asset = TestAsset.Create(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasOData("?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(0, asset));
@ -173,42 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
queryAssets = new dynamic[]
{
new
{
id = asset.Id,
version = 1,
created = asset.Created,
createdBy = "subject:user1",
lastModified = asset.LastModified,
lastModifiedBy = "subject:user2",
url = $"assets/{asset.Id}",
thumbnailUrl = $"assets/{asset.Id}?width=100",
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,
isProtected = false,
pixelWidth = 800,
pixelHeight = 600,
type = "IMAGE",
metadataText = "metadata-text",
metadataPixelWidth = 800,
metadataUnknown = (string?)null,
metadata = new
{
pixelWidth = 800,
pixelHeight = 600
},
tags = new[]
{
"tag1",
"tag2"
},
slug = "myfile.png"
}
TestAsset.Response(asset)
}
}
};
@ -219,41 +71,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[Fact]
public async Task Should_return_multiple_assets_with_total_when_querying_assets_with_total()
{
const string query = @"
var query = @"
query {
queryAssetsWithTotal(filter: ""my-query"", top: 30, skip: 5) {
total
items {
id
version
created
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
isProtected
pixelWidth
pixelHeight
type
metadataText
metadataPixelWidth: metadata(path: ""pixelWidth"")
metadataUnknown: metadata(path: ""unknown"")
metadata
tags
slug
<FIELDS>
}
}
}";
}".Replace("<FIELDS>", TestAsset.AllFields);
var asset = CreateAsset(Guid.NewGuid());
var asset = TestAsset.Create(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasOData("?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(10, asset));
@ -269,42 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
total = 10,
items = new dynamic[]
{
new
{
id = asset.Id,
version = 1,
created = asset.Created,
createdBy = "subject:user1",
lastModified = asset.LastModified,
lastModifiedBy = "subject:user2",
url = $"assets/{asset.Id}",
thumbnailUrl = $"assets/{asset.Id}?width=100",
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,
isProtected = false,
pixelWidth = 800,
pixelHeight = 600,
type = "IMAGE",
metadataText = "metadata-text",
metadataPixelWidth = 800,
metadataUnknown = (string?)null,
metadata = new
{
pixelWidth = 800,
pixelHeight = 600
},
tags = new[]
{
"tag1",
"tag2"
},
slug = "myfile.png"
}
TestAsset.Response(asset)
}
}
}
@ -345,32 +138,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_return_single_asset_when_finding_asset()
{
var assetId = Guid.NewGuid();
var asset = CreateAsset(assetId);
var asset = TestAsset.Create(assetId);
var query = @"
query {
findAsset(id: ""<ID>"") {
id
version
created
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
pixelWidth
pixelHeight
tags
slug
<FIELDS>
}
}".Replace("<ID>", assetId.ToString());
}".Replace("<ID>", assetId.ToString()).Replace("<FIELDS>", TestAsset.AllFields);
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId)))
.Returns(ResultList.CreateFrom(1, asset));
@ -381,28 +156,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
data = new
{
findAsset = new
{
id = asset.Id,
version = 1,
created = asset.Created,
createdBy = "subject:user1",
lastModified = asset.LastModified,
lastModifiedBy = "subject:user2",
url = $"assets/{asset.Id}",
thumbnailUrl = $"assets/{asset.Id}?width=100",
sourceUrl = $"assets/source/{asset.Id}",
mimeType = "image/png",
fileName = "MyFile.png",
fileHash = "ABC123",
fileSize = 1024,
fileVersion = 123,
isImage = true,
pixelWidth = 800,
pixelHeight = 600,
tags = new[] { "tag1", "tag2" },
slug = "myfile.png"
}
findAsset = TestAsset.Response(asset)
}
};
@ -442,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}";
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
var content = TestContent.Create(schemaId, Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(0, content));
@ -513,54 +267,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[Fact]
public async Task Should_return_multiple_contents_when_querying_contents()
{
const string query = @"
var query = @"
query {
queryMySchemaContents(top: 30, skip: 5) {
id
version
created
createdBy
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
de
<FIELDS>
}
myNumber {
iv
}
myBoolean {
iv
}
myDatetime {
iv
}
myJson {
iv
}
myGeolocation {
iv
}
myTags {
iv
}
myLocalized {
de_DE
}
myArray {
iv {
nestedNumber
nestedBoolean
}
}
}
}
}";
}".Replace("<FIELDS>", TestContent.AllFields);
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
var content = TestContent.Create(schemaId, Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(0, content));
@ -573,80 +287,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
queryMySchemaContents = new dynamic[]
{
new
{
id = content.Id,
version = 1,
created = content.Created,
createdBy = "subject:user1",
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
myString = new
{
de = "value"
},
myNumber = new
{
iv = 1
},
myBoolean = new
{
iv = true
},
myDatetime = new
{
iv = content.LastModified
},
myJson = new
{
iv = new
{
value = 1
}
},
myGeolocation = new
{
iv = new
{
latitude = 10,
longitude = 20
}
},
myTags = new
{
iv = new[]
{
"tag1",
"tag2"
}
},
myLocalized = new
{
de_DE = "de-DE"
},
myArray = new
{
iv = new[]
{
new
{
nestedNumber = 10,
nestedBoolean = true
},
new
{
nestedNumber = 20,
nestedBoolean = false
}
}
}
}
}
TestContent.Response(content)
}
}
};
@ -657,51 +298,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[Fact]
public async Task Should_return_multiple_contents_with_total_when_querying_contents_with_total()
{
const string query = @"
var query = @"
query {
queryMySchemaContentsWithTotal(top: 30, skip: 5) {
total
items {
id
version
created
createdBy
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
de
}
myNumber {
iv
}
myBoolean {
iv
<FIELDS>
}
myDatetime {
iv
}
myJson {
iv
}
myGeolocation {
iv
}
myTags {
iv
}
myLocalized {
de_DE
}
}
}
}
}";
}".Replace("<FIELDS>", TestContent.AllFields);
var content = CreateContent(Guid.NewGuid(), Guid.Empty, Guid.Empty);
var content = TestContent.Create(schemaId, Guid.NewGuid(), Guid.Empty, Guid.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(10, content));
@ -717,149 +324,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
total = 10,
items = new dynamic[]
{
new
{
id = content.Id,
version = 1,
created = content.Created,
createdBy = "subject:user1",
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
myString = new
{
de = "value"
},
myNumber = new
{
iv = 1
},
myBoolean = new
{
iv = true
},
myDatetime = new
{
iv = content.LastModified
},
myJson = new
{
iv = new
{
value = 1
}
},
myGeolocation = new
{
iv = new
{
latitude = 10,
longitude = 20
}
},
myTags = new
{
iv = new[]
{
"tag1",
"tag2"
}
},
myLocalized = new
{
de_DE = "de-DE"
}
}
}
}
}
}
};
AssertResult(expected, result);
}
[Fact]
public async Task Should_return_single_content_with_fixed_names()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty);
var query = @"
query {
findMySchemaContent(id: ""<ID>"") {
data {
gql_2Numbers {
iv
}
gql_2Numbers2 {
iv
}
myNumber {
iv
}
myNumber2 {
iv
}
myArray {
iv {
nestedNumber
nestedNumber2
}
}
}
}
}".Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
.Returns(ResultList.CreateFrom(1, content));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
findMySchemaContent = new
{
data = new
{
gql_2Numbers = new
{
iv = 22
},
gql_2Numbers2 = new
{
iv = 23
},
myNumber = new
{
iv = 1
},
myNumber2 = new
{
iv = 2
},
myArray = new
{
iv = new[]
{
new
{
nestedNumber = 10,
nestedNumber2 = 11
},
new
{
nestedNumber = 20,
nestedNumber2 = 21
}
}
}
TestContent.Response(content)
}
}
}
@ -900,48 +365,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_return_single_content_when_finding_content()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty);
var content = TestContent.Create(schemaId, contentId, Guid.Empty, Guid.Empty);
var query = @"
query {
findMySchemaContent(id: ""<ID>"") {
id
version
created
createdBy
lastModified
lastModifiedBy
status
statusColor
url
data {
myString {
de
}
myNumber {
iv
}
myBoolean {
iv
}
myDatetime {
iv
}
myJson {
iv
}
myGeolocation {
iv
}
myTags {
iv
}
myLocalized {
de_DE
<FIELDS>
}
}
}
}".Replace("<ID>", contentId.ToString());
}".Replace("<FIELDS>", TestContent.AllFields).Replace("<ID>", contentId.ToString());
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
.Returns(ResultList.CreateFrom(1, content));
@ -952,64 +383,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
data = new
{
findMySchemaContent = new
{
id = content.Id,
version = 1,
created = content.Created,
createdBy = "subject:user1",
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = new
{
myString = new
{
de = "value"
},
myNumber = new
{
iv = 1
},
myBoolean = new
{
iv = true
},
myDatetime = new
{
iv = content.LastModified
},
myJson = new
{
iv = new
{
value = 1
}
},
myGeolocation = new
{
iv = new
{
latitude = 10,
longitude = 20
}
},
myTags = new
{
iv = new[]
{
"tag1",
"tag2"
}
},
myLocalized = new
{
de_DE = "de-DE"
}
}
}
findMySchemaContent = TestContent.Response(content)
}
};
@ -1020,10 +394,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_also_fetch_referenced_contents_when_field_is_included_in_query()
{
var contentRefId = Guid.NewGuid();
var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, contentRefId, Guid.Empty);
var content = TestContent.Create(schemaId, contentId, contentRefId, Guid.Empty);
var query = @"
query {
@ -1090,10 +464,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_also_fetch_union_contents_when_field_is_included_in_query()
{
var contentRefId = Guid.NewGuid();
var contentRef = CreateRefContent(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, contentRefId, Guid.Empty);
var content = TestContent.Create(schemaId, contentId, contentRefId, Guid.Empty);
var query = @"
query {
@ -1166,10 +540,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query()
{
var assetRefId = Guid.NewGuid();
var assetRef = CreateAsset(assetRefId);
var assetRef = TestAsset.Create(assetRefId);
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, assetRefId);
var content = TestContent.Create(schemaId, contentId, Guid.Empty, assetRefId);
var query = @"
query {
@ -1225,8 +599,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var assetId1 = Guid.NewGuid();
var assetId2 = Guid.NewGuid();
var asset1 = CreateAsset(assetId1);
var asset2 = CreateAsset(assetId2);
var asset1 = TestAsset.Create(assetId1);
var asset2 = TestAsset.Create(assetId2);
var query1 = @"
query {
@ -1280,7 +654,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_not_return_data_when_field_not_part_of_content()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty, new NamedContentData());
var content = TestContent.Create(schemaId, contentId, Guid.Empty, Guid.Empty, new NamedContentData());
var query = @"
query {

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

@ -7,17 +7,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
using GraphQL;
using GraphQL.DataLoader;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Apps;
@ -26,8 +21,8 @@ using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
using Xunit;
@ -40,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
protected readonly IAppEntity app;
protected readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
protected readonly ICommandBus commandBus = A.Fake<ICommandBus>();
protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
protected readonly IJsonSerializer serializer = TestUtils.CreateSerializer(TypeNameHandling.None);
protected readonly ISchemaEntity schema;
@ -116,140 +112,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
sut = CreateSut();
}
protected IEnrichedContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData? data = null)
{
var now = SystemClock.Instance.GetCurrentInstant();
data ??=
new NamedContentData()
.AddField("my-string",
new ContentFieldData()
.AddValue("de", "value"))
.AddField("my-assets",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(assetId.ToString())))
.AddField("2_numbers",
new ContentFieldData()
.AddValue("iv", 22))
.AddField("2-numbers",
new ContentFieldData()
.AddValue("iv", 23))
.AddField("my-number",
new ContentFieldData()
.AddValue("iv", 1.0))
.AddField("my_number",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("my-boolean",
new ContentFieldData()
.AddValue("iv", true))
.AddField("my-datetime",
new ContentFieldData()
.AddValue("iv", now))
.AddField("my-tags",
new ContentFieldData()
.AddValue("iv", JsonValue.Array("tag1", "tag2")))
.AddField("my-references",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(refId.ToString())))
.AddField("my-union",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(refId.ToString())))
.AddField("my-geolocation",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20)))
.AddField("my-json",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("value", 1)))
.AddField("my-localized",
new ContentFieldData()
.AddValue("de-DE", "de-DE"))
.AddField("my-array",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
JsonValue.Object()
.Add("nested-boolean", true)
.Add("nested-number", 10)
.Add("nested_number", 11),
JsonValue.Object()
.Add("nested-boolean", false)
.Add("nested-number", 20)
.Add("nested_number", 21))));
var content = new ContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
SchemaId = schemaId,
Status = Status.Draft,
StatusColor = "red"
};
return content;
}
protected static IEnrichedContentEntity CreateRefContent(NamedId<Guid> schemaId, Guid id, string field, string value)
{
var now = SystemClock.Instance.GetCurrentInstant();
var data =
new NamedContentData()
.AddField(field,
new ContentFieldData()
.AddValue("iv", value));
var content = new ContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
SchemaId = schemaId,
Status = Status.Draft,
StatusColor = "red"
};
return content;
}
protected static IEnrichedAssetEntity CreateAsset(Guid id)
{
var now = SystemClock.Instance.GetCurrentInstant();
var asset = new AssetEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
FileName = "MyFile.png",
Slug = "myfile.png",
FileSize = 1024,
FileHash = "ABC123",
FileVersion = 123,
MimeType = "image/png",
Type = AssetType.Image,
MetadataText = "metadata-text",
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600),
TagNames = new[] { "tag1", "tag2" }.ToHashSet()
};
return asset;
}
protected void AssertResult(object expected, (bool HasErrors, object Response) result, bool checkErrors = true)
{
if (checkErrors && result.HasErrors)
@ -268,20 +130,25 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return serializer.Serialize(result);
}
private CachingGraphQLService CreateSut()
public sealed class TestServiceProvider : IServiceProvider
{
private readonly Dictionary<Type, object> services;
public TestServiceProvider(GraphQLTestBase testBase)
{
var appProvider = A.Fake<IAppProvider>();
A.CallTo(() => appProvider.GetSchemasAsync(appId.Id))
.Returns(new List<ISchemaEntity> { schema, schemaRef1, schemaRef2 });
A.CallTo(() => appProvider.GetSchemasAsync(testBase.appId.Id))
.Returns(new List<ISchemaEntity> { testBase.schema, testBase.schemaRef1, testBase.schemaRef2 });
var dataLoaderContext = new DataLoaderContextAccessor();
var services = new Dictionary<Type, object>
services = new Dictionary<Type, object>
{
[typeof(IAppProvider)] = appProvider,
[typeof(IAssetQueryService)] = assetQuery,
[typeof(IContentQueryService)] = contentQuery,
[typeof(IAssetQueryService)] = testBase.assetQuery,
[typeof(ICommandBus)] = testBase.commandBus,
[typeof(IContentQueryService)] = testBase.contentQuery,
[typeof(IDataLoaderContextAccessor)] = dataLoaderContext,
[typeof(IOptions<AssetOptions>)] = Options.Create(new AssetOptions()),
[typeof(IOptions<ContentOptions>)] = Options.Create(new ContentOptions()),
@ -289,12 +156,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[typeof(IUrlGenerator)] = new FakeUrlGenerator(),
[typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext)
};
}
var resolver = new FuncDependencyResolver(t => services[t]);
public object GetService(Type serviceType)
{
return services.GetOrDefault(serviceType);
}
}
private CachingGraphQLService CreateSut()
{
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
return new CachingGraphQLService(cache, resolver);
return new CachingGraphQLService(cache, new TestServiceProvider(this));
}
}
}

116
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs

@ -0,0 +1,116 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public static class TestAsset
{
public const string AllFields = @"
id
version
created
createdBy
lastModified
lastModifiedBy
url
thumbnailUrl
sourceUrl
mimeType
fileName
fileHash
fileSize
fileVersion
isImage
isProtected
pixelWidth
pixelHeight
tags
type
metadataText
metadataPixelWidth: metadata(path: ""pixelWidth"")
metadataUnknown: metadata(path: ""unknown"")
metadata
slug";
public static IEnrichedAssetEntity Create(Guid id)
{
var now = SystemClock.Instance.GetCurrentInstant();
var asset = new AssetEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
FileName = "MyFile.png",
Slug = "myfile.png",
FileSize = 1024,
FileHash = "ABC123",
FileVersion = 123,
MimeType = "image/png",
Type = AssetType.Image,
MetadataText = "metadata-text",
Metadata =
new AssetMetadata()
.SetPixelWidth(800)
.SetPixelHeight(600),
TagNames = new[]
{
"tag1",
"tag2"
}.ToHashSet()
};
return asset;
}
public static object Response(IEnrichedAssetEntity asset)
{
return new
{
id = asset.Id,
version = asset.Version,
created = asset.Created,
createdBy = asset.CreatedBy.ToString(),
lastModified = asset.LastModified,
lastModifiedBy = asset.LastModifiedBy.ToString(),
url = $"assets/{asset.Id}",
thumbnailUrl = $"assets/{asset.Id}?width=100",
sourceUrl = $"assets/source/{asset.Id}",
mimeType = asset.MimeType,
fileName = asset.FileName,
fileHash = asset.FileHash,
fileSize = asset.FileSize,
fileVersion = asset.FileVersion,
isImage = true,
isProtected = asset.IsProtected,
pixelWidth = asset.Metadata.GetPixelWidth(),
pixelHeight = asset.Metadata.GetPixelHeight(),
tags = asset.TagNames,
type = "IMAGE",
metadataText = asset.MetadataText,
metadataPixelWidth = 800,
metadataUnknown = (string?)null,
metadata = new
{
pixelWidth = asset.Metadata.GetPixelWidth(),
pixelHeight = asset.Metadata.GetPixelHeight()
},
slug = asset.Slug,
};
}
}
}

305
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs

@ -0,0 +1,305 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public static class TestContent
{
public const string AllFields = @"
id
version
created
createdBy
lastModified
lastModifiedBy
status
statusColor
url
data {
gql_2Numbers {
iv
}
gql_2Numbers2 {
iv
}
myString {
de
}
myNumber {
iv
}
myNumber2 {
iv
}
myBoolean {
iv
}
myDatetime {
iv
}
myJson {
iv
}
myGeolocation {
iv
}
myTags {
iv
}
myLocalized {
de_DE
}
myArray {
iv {
nestedNumber
nestedNumber2
nestedBoolean
}
}
}";
public static IEnrichedContentEntity Create(NamedId<Guid> schemaId, Guid id, Guid refId, Guid assetId, NamedContentData? data = null)
{
var now = SystemClock.Instance.GetCurrentInstant();
data ??=
new NamedContentData()
.AddField("my-string",
new ContentFieldData()
.AddValue("de", "value"))
.AddField("my-assets",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(assetId.ToString())))
.AddField("2_numbers",
new ContentFieldData()
.AddValue("iv", 22))
.AddField("2-numbers",
new ContentFieldData()
.AddValue("iv", 23))
.AddField("my-number",
new ContentFieldData()
.AddValue("iv", 1.0))
.AddField("my_number",
new ContentFieldData()
.AddValue("iv", 2.0))
.AddField("my-boolean",
new ContentFieldData()
.AddValue("iv", true))
.AddField("my-datetime",
new ContentFieldData()
.AddValue("iv", now))
.AddField("my-tags",
new ContentFieldData()
.AddValue("iv", JsonValue.Array("tag1", "tag2")))
.AddField("my-references",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(refId.ToString())))
.AddField("my-union",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(refId.ToString())))
.AddField("my-geolocation",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("latitude", 10).Add("longitude", 20)))
.AddField("my-json",
new ContentFieldData()
.AddValue("iv", JsonValue.Object().Add("value", 1)))
.AddField("my-localized",
new ContentFieldData()
.AddValue("de-DE", "de-DE"))
.AddField("my-array",
new ContentFieldData()
.AddValue("iv", JsonValue.Array(
JsonValue.Object()
.Add("nested-boolean", true)
.Add("nested-number", 10)
.Add("nested_number", 11),
JsonValue.Object()
.Add("nested-boolean", false)
.Add("nested-number", 20)
.Add("nested_number", 21))));
var content = new ContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
SchemaId = schemaId,
Status = Status.Draft,
StatusColor = "red"
};
return content;
}
public static IEnrichedContentEntity CreateRef(NamedId<Guid> schemaId, Guid id, string field, string value)
{
var now = SystemClock.Instance.GetCurrentInstant();
var data =
new NamedContentData()
.AddField(field,
new ContentFieldData()
.AddValue("iv", value));
var content = new ContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
LastModified = now,
LastModifiedBy = new RefToken(RefTokenType.Subject, "user2"),
Data = data,
SchemaId = schemaId,
Status = Status.Draft,
StatusColor = "red"
};
return content;
}
public static object Response(IEnrichedContentEntity content)
{
return new
{
id = content.Id,
version = 1,
created = content.Created,
createdBy = "subject:user1",
lastModified = content.LastModified,
lastModifiedBy = "subject:user2",
status = "DRAFT",
statusColor = "red",
url = $"contents/my-schema/{content.Id}",
data = Data(content)
};
}
public static object Data(IContentEntity content, Guid refId = default, Guid assetId = default)
{
var result = new Dictionary<string, object>
{
["gql_2Numbers"] = new
{
iv = 22
},
["gql_2Numbers2"] = new
{
iv = 23
},
["myString"] = new
{
de = "value"
},
["myNumber"] = new
{
iv = 1
},
["myNumber2"] = new
{
iv = 2
},
["myBoolean"] = new
{
iv = true
},
["myDatetime"] = new
{
iv = content.LastModified.ToString()
},
["myJson"] = new
{
iv = new
{
value = 1
}
},
["myGeolocation"] = new
{
iv = new
{
latitude = 10,
longitude = 20
}
},
["myTags"] = new
{
iv = new[]
{
"tag1",
"tag2"
}
},
["myLocalized"] = new
{
de_DE = "de-DE"
},
["myArray"] = new
{
iv = new[]
{
new
{
nestedNumber = 10,
nestedNumber2 = 11,
nestedBoolean = true
},
new
{
nestedNumber = 20,
nestedNumber2 = 21,
nestedBoolean = false
}
}
}
};
if (refId != default)
{
result["myReferences"] = new
{
iv = new[]
{
refId
}
};
result["myUnion"] = new
{
iv = new[]
{
refId
}
};
}
if (assetId != default)
{
result["myAssets"] = new
{
iv = new[]
{
assetId
}
};
}
return result;
}
}
}

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -19,7 +19,8 @@
<ItemGroup>
<PackageReference Include="FakeItEasy" Version="6.0.1" />
<PackageReference Include="FluentAssertions" Version="5.10.3" />
<PackageReference Include="GraphQL" Version="2.4.0" />
<PackageReference Include="GraphQL" Version="3.0.0" />
<PackageReference Include="GraphQL.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Lorem.Universal.Net" Version="3.0.64" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />

64
backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs

@ -179,6 +179,70 @@ namespace TestSuite.ApiTests
AssertItems(items, 3, new[] { 4, 5, 6 });
}
[Fact]
public async Task Should_create_and_query_with_inline_graphql()
{
var query = new
{
query = @"
mutation {
createMyReadsContent(data: {
number: {
iv: 999
}
}) {
id,
data {
number {
iv
}
}
}
}"
};
var result = await _.Contents.GraphQlAsync<JObject>(query);
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>();
Assert.Equal(999, value);
}
[Fact]
public async Task Should_create_and_query_with_variable_graphql()
{
var query = new
{
query = @"
mutation Mutation($data: MyReadsDataInputDto!) {
createMyReadsContent(data: $data) {
id,
data {
number {
iv
}
}
}
}",
variables = new
{
data = new
{
number = new
{
iv = 998
}
}
}
};
var result = await _.Contents.GraphQlAsync<JObject>(query);
var value = result["createMyReadsContent"]["data"]["number"]["iv"].Value<int>();
Assert.Equal(998, value);
}
[Fact]
public async Task Should_query_items_with_graphql()
{

6
backend/tools/TestSuite/TestSuite.ApiTests/GraphQLTests.cs

@ -21,11 +21,11 @@ using Xunit;
namespace TestSuite.ApiTests
{
[Trait("Category", "NotAutomated")]
public sealed class GraphQLTests : IClassFixture<CreatedAppFixture>
public sealed class GraphQLTests : IClassFixture<ContentFixture>
{
public CreatedAppFixture _ { get; }
public ContentFixture _ { get; }
public GraphQLTests(CreatedAppFixture fixture)
public GraphQLTests(ContentFixture fixture)
{
_ = fixture;
}

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

@ -0,0 +1,344 @@
// ==========================================================================
// 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 Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class AppMutationsGraphType : ObjectGraphType
{
public AppMutationsGraphType(IGraphModel model, IEnumerable<ISchemaEntity> 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 contentDataType = model.GetContentDataType(schema.Id);
var resultType = new ContentDataChangedResultGraphType(schemaType, schemaName, contentDataType);
var inputType = new ContentDataGraphInputType(model, schema);
AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType);
AddContentUpdate(schemaType, schemaName, inputType, resultType);
AddContentPatch(schemaType, schemaName, inputType, resultType);
AddContentPublish(schemaType, schemaName);
AddContentUnpublish(schemaType, schemaName);
AddContentArchive(schemaType, schemaName);
AddContentRestore(schemaType, schemaName);
AddContentDelete(schemaType, schemaName);
}
Description = "The app mutations.";
}
private void AddContentCreate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType contentDataType, IGraphType contentType)
{
AddField(new FieldType
{
Name = $"create{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} 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
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(contentType),
Resolver = ResolveAsync(async (c, publish) =>
{
var argPublish = c.GetArgument<bool>("publish");
var contentData = GetContentData(c);
var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish };
var commandContext = await publish(command);
var result = commandContext.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentEntity.Create(command, result);
return (IContentEntity)ContentEntity.Create(command, result);
}),
Description = $"Creates an {schemaName} content."
});
}
private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"update{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new UpdateContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Update an {schemaName} content by id."
});
}
private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IGraphType resultType)
{
AddField(new FieldType
{
Name = $"patch{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(AllTypes.None)
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty,
ResolvedType = AllTypes.NonNullGuid
},
new QueryArgument(AllTypes.None)
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
},
ResolvedType = new NonNullGraphType(resultType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new PatchContent { ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result;
}),
Description = $"Patch a {schemaName} content."
});
}
private void AddContentPublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"publish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published };
return publish(command);
}),
Description = $"Publish a {schemaName} content."
});
}
private void AddContentUnpublish(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"unpublish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Unpublish a {schemaName} content."
});
}
private void AddContentArchive(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"archive{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived };
return publish(command);
}),
Description = $"Archive a {schemaName} content."
});
}
private void AddContentRestore(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"restore{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Restore a {schemaName} content."
});
}
private void AddContentDelete(string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"delete{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = AllTypes.CommandVersion,
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new DeleteContent { ContentId = contentId };
return publish(command);
}),
Description = $"Delete an {schemaName} content."
});
}
private static QueryArguments CreateIdArguments(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
},
new QueryArgument(AllTypes.None)
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any,
ResolvedType = AllTypes.Int
}
};
}
private static IFieldResolver ResolveAsync<T>(Func<ResolveFieldContext, Func<SquidexCommand, Task<CommandContext>>, Task<T>> action)
{
return new FuncFieldResolver<Task<T>>(async c =>
{
var e = (GraphQLExecutionContext)c.UserContext;
try
{
return await action(c, command =>
{
command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any);
return e.CommandBus.PublishAsync(command);
});
}
catch (ValidationException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
catch (DomainException ex)
{
c.Errors.Add(new ExecutionError(ex.Message));
throw;
}
});
}
private static NamedContentData GetContentData(ResolveFieldContext c)
{
return JObject.FromObject(c.GetArgument<object>("data")).ToObject<NamedContentData>();
}
}
}

70
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs

@ -0,0 +1,70 @@
// ==========================================================================
// 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.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentDataGraphInputType : InputObjectGraphType
{
public ContentDataGraphInputType(IGraphModel model, ISchemaEntity schema)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
Name = $"{schemaType}InputDto";
foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden))
{
var inputType = model.GetInputGraphType(field);
if (inputType != null)
{
if (field.RawProperties.IsRequired)
{
inputType = new NonNullGraphType(inputType);
}
var fieldName = field.RawProperties.Label.WithFallback(field.Name);
var fieldGraphType = new InputObjectGraphType
{
Name = $"{schemaType}Data{field.Name.ToPascalCase()}InputDto"
};
var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition)
{
fieldGraphType.AddField(new FieldType
{
Name = partitionItem.Key,
Resolver = null,
ResolvedType = inputType,
Description = field.RawProperties.Hints
});
}
fieldGraphType.Description = $"The input structure of the {fieldName} of a {schemaName} content type.";
AddField(new FieldType
{
Name = field.Name.ToCamelCase(),
Resolver = null,
ResolvedType = fieldGraphType,
Description = $"The {fieldName} field."
});
}
}
Description = $"The structure of a {schemaName} content type.";
}
}
}

31
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs

@ -0,0 +1,31 @@
// ==========================================================================
// 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
{
public sealed class GeolocationInputGraphType : InputObjectGraphType
{
public GeolocationInputGraphType()
{
Name = "GeolocationInputDto";
AddField(new FieldType
{
Name = "latitude",
ResolvedType = AllTypes.NonNullFloat
});
AddField(new FieldType
{
Name = "longitude",
ResolvedType = AllTypes.NonNullFloat
});
}
}
}

20
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldExtensions.cs

@ -0,0 +1,20 @@
// ==========================================================================
// 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;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static class InputFieldExtensions
{
public static IGraphType GetInputGraphType(this IField field)
{
return field.Accept(InputFieldVisitor.Default);
}
}
}

71
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/InputFieldVisitor.cs

@ -0,0 +1,71 @@
// ==========================================================================
// 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;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class InputFieldVisitor : IFieldVisitor<IGraphType>
{
public static readonly InputFieldVisitor Default = new InputFieldVisitor();
private InputFieldVisitor()
{
}
public IGraphType Visit(IArrayField field)
{
return AllTypes.NoopArray;
}
public IGraphType Visit(IField<AssetsFieldProperties> field)
{
return AllTypes.References;
}
public IGraphType Visit(IField<BooleanFieldProperties> field)
{
return AllTypes.Boolean;
}
public IGraphType Visit(IField<DateTimeFieldProperties> field)
{
return AllTypes.Date;
}
public IGraphType Visit(IField<GeolocationFieldProperties> field)
{
return AllTypes.GeolocationInput;
}
public IGraphType Visit(IField<JsonFieldProperties> field)
{
return AllTypes.Json;
}
public IGraphType Visit(IField<NumberFieldProperties> field)
{
return AllTypes.Float;
}
public IGraphType Visit(IField<ReferencesFieldProperties> field)
{
return AllTypes.References;
}
public IGraphType Visit(IField<StringFieldProperties> field)
{
return AllTypes.String;
}
public IGraphType Visit(IField<TagsFieldProperties> field)
{
return AllTypes.Tags;
}
}
}

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

@ -0,0 +1,47 @@
// ==========================================================================
// 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;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class NestedInputGraphType : InputObjectGraphType
{
public NestedInputGraphType(IGraphModel model, ISchemaEntity schema, IArrayField field)
{
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
var fieldType = field.TypeName();
var fieldName = field.DisplayName();
Name = $"{schemaType}{fieldName}ChildDto";
foreach (var nestedField in field.Fields.Where(x => !x.IsHidden))
{
var fieldInfo = model.GetGraphType(schema, nestedField);
if (fieldInfo.ResolveType != null)
{
AddField(new FieldType
{
Name = nestedField.Name.ToCamelCase(),
Resolver = null,
ResolvedType = fieldInfo.ResolveType,
Description = $"The {fieldName}/{nestedField.DisplayName()} nested field."
});
}
}
Description = $"The structure of a {schemaName}.{fieldName} nested schema.";
}
}
}
Loading…
Cancel
Save