Browse Source

Mutation endpoints started

pull/222/head
Sebastian Stehle 8 years ago
parent
commit
6e1e873656
  1. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  2. 3
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
  3. 56
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  4. 21
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  5. 12
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  6. 9
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  7. 115
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  8. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
  9. 318
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
  10. 107
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppQueriesGraphType.cs
  11. 40
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  12. 14
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs
  13. 44
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/CommandVersionGraphType.cs
  14. 73
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphInputType.cs
  15. 17
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs
  16. 41
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  17. 13
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs
  18. 31
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GeolocationInputGraphType.cs
  19. 55
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GuidGraphType.cs
  20. 41
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs
  21. 3
      src/Squidex.Domain.Apps.Entities/SquidexCommand.cs
  22. 1
      src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  23. 8
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  24. 18
      src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs
  25. 35
      src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs
  26. 2
      src/Squidex/Areas/Api/Controllers/Content/Models/ContentsDto.cs
  27. 18
      src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs
  28. 207
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  29. 167
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  30. 169
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  31. 35
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs

1
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;

3
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs

@ -6,15 +6,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Security.Claims;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
{ {
public abstract class ContentCommand : SchemaCommand, IAggregateCommand public abstract class ContentCommand : SchemaCommand, IAggregateCommand
{ {
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; } public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId Guid IAggregateCommand.AggregateId

56
src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs

@ -0,0 +1,56 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentEntity : IContentEntity
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; }
public Status Status { get; set; }
public static ContentEntity Create(CreateContent command, EntityCreatedResult<NamedContentData> result)
{
var now = SystemClock.Instance.GetCurrentInstant();
var response = new ContentEntity
{
Id = command.ContentId,
Data = result.IdOrValue,
Version = result.Version,
Created = now,
CreatedBy = command.Actor,
LastModified = now,
LastModifiedBy = command.Actor,
Status = command.Publish ? Status.Published : Status.Draft
};
return response;
}
}
}

21
src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs

@ -12,7 +12,6 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData; using Microsoft.OData;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
@ -122,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var content in contents) foreach (var content in contents)
{ {
var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText); var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText);
var contentResult = SimpleMapper.Map(content, new Content()); var contentResult = SimpleMapper.Map(content, new ContentEntity());
contentResult.Data = contentData; contentResult.Data = contentData;
@ -199,23 +198,5 @@ namespace Squidex.Domain.Apps.Entities.Contents
return status; return status;
} }
private sealed class Content : IContentEntity
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; }
public Status Status { get; set; }
}
} }
} }

12
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -13,6 +13,7 @@ using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery; private readonly IContentQueryService contentQuery;
private readonly ICommandBus commandBus;
private readonly IGraphQLUrlGenerator urlGenerator; private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository; private readonly IAssetRepository assetRepository;
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
@ -27,17 +29,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public CachingGraphQLService(IMemoryCache cache, public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider, IAppProvider appProvider,
IAssetRepository assetRepository, IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery, IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator) IGraphQLUrlGenerator urlGenerator)
: base(cache) : base(cache)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentQuery, nameof(urlGenerator)); Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(contentQuery, nameof(contentQuery)); Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider; this.appProvider = appProvider;
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.commandBus = commandBus;
this.contentQuery = contentQuery; this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator; this.urlGenerator = urlGenerator;
} }
@ -53,9 +58,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
var modelContext = await GetModelAsync(app); var modelContext = await GetModelAsync(app);
var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator);
return await modelContext.ExecuteAsync(queryContext, query); var ctx = new GraphQLExecutionContext(app, assetRepository, commandBus, contentQuery, user, urlGenerator);
return await modelContext.ExecuteAsync(ctx, query);
} }
private async Task<GraphQLModel> GetModelAsync(IAppEntity app) private async Task<GraphQLModel> GetModelAsync(IAppEntity app)

9
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs → src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -13,17 +13,22 @@ using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLQueryContext : QueryContext public sealed class GraphQLExecutionContext : QueryContext
{ {
public ICommandBus CommandBus { get; }
public IGraphQLUrlGenerator UrlGenerator { get; } public IGraphQLUrlGenerator UrlGenerator { get; }
public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user, public GraphQLExecutionContext(IAppEntity app, IAssetRepository assetRepository, ICommandBus commandBus, IContentQueryService contentQuery, ClaimsPrincipal user,
IGraphQLUrlGenerator urlGenerator) IGraphQLUrlGenerator urlGenerator)
: base(app, assetRepository, contentQuery, user) : base(app, assetRepository, contentQuery, user)
{ {
CommandBus = commandBus;
UrlGenerator = urlGenerator; UrlGenerator = urlGenerator;
} }

115
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -24,15 +24,17 @@ using GraphQLSchema = GraphQL.Types.Schema;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public sealed class GraphQLModel : IGraphQLContext public sealed class GraphQLModel : IGraphModel
{ {
private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos; private readonly Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos;
private readonly Dictionary<Guid, ContentGraphType> schemaTypes = new Dictionary<Guid, ContentGraphType>(); private readonly Dictionary<Type, IGraphType> inputFieldInfos;
private readonly Dictionary<ISchemaEntity, ContentGraphType> contentTypes = new Dictionary<ISchemaEntity, ContentGraphType>();
private readonly Dictionary<ISchemaEntity, ContentDataGraphType> contentDataTypes = new Dictionary<ISchemaEntity, ContentDataGraphType>();
private readonly Dictionary<Guid, ISchemaEntity> schemas; private readonly Dictionary<Guid, ISchemaEntity> schemas;
private readonly PartitionResolver partitionResolver; private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IGraphType assetType;
private readonly IGraphType assetListType; private readonly IGraphType assetListType;
private readonly IComplexGraphType assetType;
private readonly GraphQLSchema graphQLSchema; private readonly GraphQLSchema graphQLSchema;
public bool CanGenerateAssetSourceUrl { get; } public bool CanGenerateAssetSourceUrl { get; }
@ -48,6 +50,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this); assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType)); assetListType = new ListGraphType(new NonNullGraphType(assetType));
inputFieldInfos = new Dictionary<Type, IGraphType>
{
{
typeof(StringField),
new StringGraphType()
},
{
typeof(BooleanField),
new BooleanGraphType()
},
{
typeof(NumberField),
new FloatGraphType()
},
{
typeof(DateTimeField),
new DateGraphType()
},
{
typeof(GeolocationField),
new GeolocationInputGraphType()
},
{
typeof(TagsField),
new ListGraphType(new StringGraphType())
},
{
typeof(AssetsField),
new ListGraphType(new GuidGraphType())
},
{
typeof(ReferencesField),
new ListGraphType(new GuidGraphType())
}
};
fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>> fieldInfos = new Dictionary<Type, Func<Field, (IGraphType ResolveType, IFieldResolver Resolver)>>
{ {
{ {
@ -70,14 +108,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
typeof(JsonField), typeof(JsonField),
field => ResolveDefault("Json") field => ResolveDefault("Json")
}, },
{
typeof(TagsField),
field => ResolveDefault("String")
},
{ {
typeof(GeolocationField), typeof(GeolocationField),
field => ResolveDefault("Geolocation") field => ResolveDefault("Geolocation")
}, },
{
typeof(TagsField),
field => ResolveDefault("String")
},
{ {
typeof(AssetsField), typeof(AssetsField),
field => ResolveAssets(assetListType) field => ResolveAssets(assetListType)
@ -90,11 +128,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.schemas = schemas.ToDictionary(x => x.Id); this.schemas = schemas.ToDictionary(x => x.Id);
graphQLSchema = new GraphQLSchema { Query = new AppQueriesGraphType(this, this.schemas.Values) }; var m = new AppMutationsGraphType(this, this.schemas.Values);
var q = new AppQueriesGraphType(this, this.schemas.Values);
graphQLSchema = new GraphQLSchema { Query = q, Mutation = m };
foreach (var schemaType in schemaTypes.Values) foreach (var kvp in contentDataTypes)
{ {
schemaType.Initialize(); kvp.Value.Initialize(this, kvp.Key);
}
foreach (var kvp in contentTypes)
{
kvp.Value.Initialize(this, kvp.Key, contentDataTypes[kvp.Key]);
} }
} }
@ -107,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetUrl(app, c.Source); return context.UrlGenerator.GenerateAssetUrl(app, c.Source);
}); });
@ -119,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source); return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source);
}); });
@ -131,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var resolver = new FuncFieldResolver<IAssetEntity, object>(c => var resolver = new FuncFieldResolver<IAssetEntity, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source); return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source);
}); });
@ -143,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var resolver = new FuncFieldResolver<IContentEntity, object>(c => var resolver = new FuncFieldResolver<IContentEntity, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source); return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source);
}); });
@ -155,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName); var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedAssetsAsync(contentIds); return context.GetReferencedAssetsAsync(contentIds);
@ -167,27 +213,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(Field field) private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(Field field)
{ {
var schemaId = ((ReferencesField)field).Properties.SchemaId; var schemaId = ((ReferencesField)field).Properties.SchemaId;
var schemaType = GetSchemaType(schemaId);
if (schemaType == null) var contentType = GetContentType(schemaId);
if (contentType == null)
{ {
return (null, null); return (null, null);
} }
var resolver = new FuncFieldResolver<ContentFieldData, object>(c => var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName); var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedContentsAsync(schemaId, contentIds); return context.GetReferencedContentsAsync(schemaId, contentIds);
}); });
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId))); var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));
return (schemaFieldType, resolver); return (schemaFieldType, resolver);
} }
public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query) public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query)
{ {
Guard.NotNull(context, nameof(context)); Guard.NotNull(context, nameof(context));
@ -208,7 +255,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key); return partitionResolver(key);
} }
public IGraphType GetAssetType() public IComplexGraphType GetAssetType()
{ {
return assetType; return assetType;
} }
@ -218,11 +265,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return fieldInfos[field.GetType()](field); return fieldInfos[field.GetType()](field);
} }
public IGraphType GetSchemaType(Guid schemaId) public IComplexGraphType GetContentDataType(Guid schemaId)
{ {
var schema = schemas.GetOrDefault(schemaId); var schema = schemas.GetOrDefault(schemaId);
return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null; if (schema == null)
{
return null;
}
return schema != null ? contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType()) : null;
}
public IComplexGraphType GetContentType(Guid schemaId)
{
var schema = schemas.GetOrDefault(schemaId);
if (schema == null)
{
return null;
}
return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
}
public IGraphType GetInputGraphType(Field field)
{
return inputFieldInfos.GetOrAddDefault(field.GetType());
} }
} }
} }

10
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs → src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs

@ -14,15 +14,17 @@ using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public interface IGraphQLContext public interface IGraphModel
{ {
bool CanGenerateAssetSourceUrl { get; } bool CanGenerateAssetSourceUrl { get; }
IFieldPartitioning ResolvePartition(Partitioning key); IFieldPartitioning ResolvePartition(Partitioning key);
IGraphType GetAssetType(); IComplexGraphType GetAssetType();
IGraphType GetSchemaType(Guid schemaId); IComplexGraphType GetContentType(Guid schemaId);
IComplexGraphType GetContentDataType(Guid schemaId);
IFieldResolver ResolveAssetUrl(); IFieldResolver ResolveAssetUrl();
@ -32,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IFieldResolver ResolveContentUrl(ISchemaEntity schema); IFieldResolver ResolveContentUrl(ISchemaEntity schema);
IGraphType GetInputGraphType(Field field);
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field); (IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field);
} }
} }

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

@ -0,0 +1,318 @@
// ==========================================================================
// 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.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 inputType = new ContentDataGraphInputType(model, schema);
AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType);
AddContentUpdate(schemaId, schemaType, schemaName, inputType, contentDataType);
AddContentPatch(schemaId, schemaType, schemaName, inputType, contentDataType);
AddContentPublish(schemaId, schemaType, schemaName);
AddContentUnpublish(schemaId, schemaType, schemaName);
AddContentArchive(schemaId, schemaType, schemaName);
AddContentRestore(schemaId, schemaType, schemaName);
AddContentDelete(schemaId, schemaType, schemaName);
}
Description = "The app mutations.";
}
private void AddContentCreate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType contentDataType, IComplexGraphType contentType)
{
AddField(new FieldType
{
Name = $"create{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(typeof(BooleanGraphType))
{
Name = "publish",
Description = "Set to true to autopublish content.",
DefaultValue = false
},
new QueryArgument(typeof(NoopGraphType))
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(typeof(IntGraphType))
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any
}
},
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, ContentId = Guid.NewGuid(), Data = contentData, Publish = argPublish };
var commandContext = await publish(command);
var result = commandContext.Result<EntityCreatedResult<NamedContentData>>();
var response = ContentEntity.Create(command, result);
return ContentEntity.Create(command, result);
}),
Description = $"Creates an {schemaName} content."
});
}
private void AddContentUpdate(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType contentDataType)
{
AddField(new FieldType
{
Name = $"update{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty
},
new QueryArgument(typeof(NoopGraphType))
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(typeof(IntGraphType))
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any
}
},
ResolvedType = new NonNullGraphType(contentDataType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new UpdateContent { SchemaId = schemaId, ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result.Data;
}),
Description = $"Update an {schemaName} content by id."
});
}
private void AddContentPatch(NamedId<Guid> schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType contentDataType)
{
AddField(new FieldType
{
Name = $"patch{schemaType}Content",
Arguments = new QueryArguments
{
new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty
},
new QueryArgument(typeof(NoopGraphType))
{
Name = "data",
Description = $"The data for the {schemaName} content.",
DefaultValue = null,
ResolvedType = new NonNullGraphType(inputType),
},
new QueryArgument(typeof(IntGraphType))
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any
}
},
ResolvedType = new NonNullGraphType(contentDataType),
Resolver = ResolveAsync(async (c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var contentData = GetContentData(c);
var command = new PatchContent { SchemaId = schemaId, ContentId = contentId, Data = contentData };
var commandContext = await publish(command);
var result = commandContext.Result<ContentDataChangedResult>();
return result.Data;
}),
Description = $"Patch a {schemaName} content."
});
}
private void AddContentPublish(NamedId<Guid> schemaId, string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"publish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = new NonNullGraphType(new CommandVersionGraphType()),
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Published };
return publish(command);
}),
Description = $"Publish a {schemaName} content."
});
}
private void AddContentUnpublish(NamedId<Guid> schemaId, string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"unpublish{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = new NonNullGraphType(new CommandVersionGraphType()),
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Unpublish a {schemaName} content."
});
}
private void AddContentArchive(NamedId<Guid> schemaId, string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"archive{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = new NonNullGraphType(new CommandVersionGraphType()),
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Archived };
return publish(command);
}),
Description = $"Archive a {schemaName} content."
});
}
private void AddContentRestore(NamedId<Guid> schemaId, string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"restore{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = new NonNullGraphType(new CommandVersionGraphType()),
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new ChangeContentStatus { SchemaId = schemaId, ContentId = contentId, Status = Status.Draft };
return publish(command);
}),
Description = $"Restore a {schemaName} content."
});
}
private void AddContentDelete(NamedId<Guid> schemaId, string schemaType, string schemaName)
{
AddField(new FieldType
{
Name = $"delete{schemaType}Content",
Arguments = CreateIdArguments(schemaName),
ResolvedType = new NonNullGraphType(new CommandVersionGraphType()),
Resolver = ResolveAsync((c, publish) =>
{
var contentId = c.GetArgument<Guid>("id");
var command = new DeleteContent { SchemaId = schemaId, ContentId = contentId };
return publish(command);
}),
Description = $"Delete an {schemaName} content."
});
}
private static QueryArguments CreateIdArguments(string schemaName)
{
return new QueryArguments
{
new QueryArgument(typeof(GuidGraphType))
{
Name = "id",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty
},
new QueryArgument(typeof(IntGraphType))
{
Name = "expectedVersion",
Description = "The expected version",
DefaultValue = EtagVersion.Any
}
};
}
private static IFieldResolver ResolveAsync<T>(Func<ResolveFieldContext, Func<SquidexCommand, Task<CommandContext>>, Task<T>> action)
{
return new FuncFieldResolver<Task<T>>(c =>
{
var e = (GraphQLExecutionContext)c.UserContext;
return action(c, command =>
{
command.ExpectedVersion = c.GetArgument<int>("expectedVersion");
return e.CommandBus.PublishAsync(command);
});
});
}
private static NamedContentData GetContentData(ResolveFieldContext c)
{
return JObject.FromObject(c.GetArgument<object>("data")).ToObject<NamedContentData>();
}
}
}

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

@ -8,29 +8,32 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class AppQueriesGraphType : ObjectGraphType public sealed class AppQueriesGraphType : ObjectGraphType
{ {
public AppQueriesGraphType(IGraphQLContext ctx, IEnumerable<ISchemaEntity> schemas) public AppQueriesGraphType(IGraphModel model, IEnumerable<ISchemaEntity> schemas)
{ {
var assetType = ctx.GetAssetType(); var assetType = model.GetAssetType();
AddAssetFind(assetType); AddAssetFind(assetType);
AddAssetsQueries(assetType); AddAssetsQueries(assetType);
foreach (var schema in schemas) foreach (var schema in schemas)
{ {
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name); var schemaId = schema.Id;
var schemaType = ctx.GetSchemaType(schema.Id); var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
AddContentFind(schema, schemaType, schemaName); var contentType = model.GetContentType(schema.Id);
AddContentQueries(ctx, schema, schemaType, schemaName);
AddContentFind(schemaId, schemaType, schemaName, contentType);
AddContentQueries(schemaId, schemaType, schemaName, contentType);
} }
Description = "The app queries."; Description = "The app queries.";
@ -43,102 +46,94 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "findAsset", Name = "findAsset",
Arguments = CreateAssetFindArguments(), Arguments = CreateAssetFindArguments(),
ResolvedType = assetType, ResolvedType = assetType,
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var assetId = c.GetArgument<Guid>("id");
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString()));
return context.FindAssetAsync(contentId); return e.FindAssetAsync(assetId);
}), }),
Description = "Find an asset by id." Description = "Find an asset by id."
}); });
} }
private void AddContentFind(ISchemaEntity schema, IGraphType schemaType, string schemaName) private void AddContentFind(Guid schemaId, string schemaType, string schemaName, IGraphType contentType)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = $"find{schema.Name.ToPascalCase()}Content", Name = $"find{schemaType}Content",
Arguments = CreateContentFindTypes(schemaName), Arguments = CreateContentFindTypes(schemaName),
ResolvedType = schemaType, ResolvedType = contentType,
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var contentId = c.GetArgument<Guid>("id");
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString()));
return context.FindContentAsync(schema.Id, contentId); return e.FindContentAsync(schemaId, contentId);
}), }),
Description = $"Find an {schemaName} content by id." Description = $"Find an {schemaName} content by id."
}); });
} }
private void AddAssetsQueries(IGraphType assetType) private void AddAssetsQueries(IComplexGraphType assetType)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = "queryAssets", Name = "queryAssets",
Arguments = CreateAssetQueryArguments(), Arguments = CreateAssetQueryArguments(),
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var argTake = c.GetArgument("take", 20);
var argTop = c.GetArgument("top", 20);
var argSkip = c.GetArgument("skip", 0); var argSkip = c.GetArgument("skip", 0);
var argQuery = c.GetArgument("search", string.Empty); var argQuery = c.GetArgument("search", string.Empty);
return context.QueryAssetsAsync(argQuery, argSkip, argTop); return e.QueryAssetsAsync(argQuery, argSkip, argTake);
}), }),
Description = "Query assets items." Description = "Get assets."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "queryAssetsWithTotal", Name = "queryAssetsWithTotal",
Arguments = CreateAssetQueryArguments(), Arguments = CreateAssetQueryArguments(),
ResolvedType = new AssetResultGraphType(assetType), ResolvedType = new AssetsResultGraphType(assetType),
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext; var argTake = c.GetArgument("take", 20);
var argTop = c.GetArgument("top", 20);
var argSkip = c.GetArgument("skip", 0); var argSkip = c.GetArgument("skip", 0);
var argQuery = c.GetArgument("search", string.Empty); var argQuery = c.GetArgument("search", string.Empty);
return context.QueryAssetsAsync(argQuery, argSkip, argTop); return e.QueryAssetsAsync(argQuery, argSkip, argTake);
}), }),
Description = "Query assets items with total count." Description = "Get assets and total count."
}); });
} }
private void AddContentQueries(IGraphQLContext ctx, ISchemaEntity schema, IGraphType schemaType, string schemaName) private void AddContentQueries(Guid schemaId, string schemaType, string schemaName, IComplexGraphType contentType)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = $"query{schema.Name.ToPascalCase()}Contents", Name = $"query{schemaType}Contents",
Arguments = CreateContentQueryArguments(), Arguments = CreateContentQueryArguments(),
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext;
var contentQuery = BuildODataQuery(c); var contentQuery = BuildODataQuery(c);
return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); return e.QueryContentsAsync(schemaId.ToString(), contentQuery);
}), }),
Description = $"Query {schemaName} content items." Description = $"Query {schemaName} content items."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = $"query{schema.Name.ToPascalCase()}ContentsWithTotal", Name = $"query{schemaType}ContentsWithTotal",
Arguments = CreateContentQueryArguments(), Arguments = CreateContentQueryArguments(),
ResolvedType = new ContentResultGraphType(ctx, schema, schemaName), ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType),
Resolver = new FuncFieldResolver<object>(c => Resolver = ResolveAsync((c, e) =>
{ {
var context = (GraphQLQueryContext)c.UserContext;
var contentQuery = BuildODataQuery(c); var contentQuery = BuildODataQuery(c);
return context.QueryContentsAsync(schema.Id.ToString(), contentQuery); return e.QueryContentsAsync(schemaId.ToString(), contentQuery);
}), }),
Description = $"Query {schemaName} content items with total count." Description = $"Query {schemaName} content items with total count."
}); });
@ -148,10 +143,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
return new QueryArguments return new QueryArguments
{ {
new QueryArgument(typeof(StringGraphType)) new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{ {
Name = "id", Name = "id",
Description = "The id of the asset.", Description = "The id of the asset (GUID).",
DefaultValue = string.Empty DefaultValue = string.Empty
} }
}; };
@ -161,10 +156,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
return new QueryArguments return new QueryArguments
{ {
new QueryArgument(typeof(StringGraphType)) new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{ {
Name = "id", Name = "id",
Description = $"The id of the {schemaName} content.", Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty DefaultValue = string.Empty
} }
}; };
@ -176,8 +171,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
new QueryArgument(typeof(IntGraphType)) new QueryArgument(typeof(IntGraphType))
{ {
Name = "top", Name = "take",
Description = "Optional number of assets to take.", Description = "Optional number of assets to take (Default: 20).",
DefaultValue = 20 DefaultValue = 20
}, },
new QueryArgument(typeof(IntGraphType)) new QueryArgument(typeof(IntGraphType))
@ -189,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(typeof(StringGraphType)) new QueryArgument(typeof(StringGraphType))
{ {
Name = "search", Name = "search",
Description = "Optional query.", Description = "Optional query to limit the files by name.",
DefaultValue = string.Empty DefaultValue = string.Empty
} }
}; };
@ -202,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(typeof(IntGraphType)) new QueryArgument(typeof(IntGraphType))
{ {
Name = "top", Name = "top",
Description = "Optional number of contents to take.", Description = "Optional number of contents to take (Default: 20).",
DefaultValue = 20 DefaultValue = 20
}, },
new QueryArgument(typeof(IntGraphType)) new QueryArgument(typeof(IntGraphType))
@ -242,5 +237,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
return odataQuery; 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);
});
}
} }
} }

40
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -15,145 +15,145 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity> public sealed class AssetGraphType : ObjectGraphType<IAssetEntity>
{ {
public AssetGraphType(IGraphQLContext context) public AssetGraphType(IGraphModel model)
{ {
Name = "AssetDto"; Name = "AssetDto";
AddField(new FieldType AddField(new FieldType
{ {
Name = "id", Name = "id",
Resolver = Resolver(x => x.Id.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.Id.ToString()),
Description = "The id of the asset." Description = "The id of the asset."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "version", Name = "version",
Resolver = Resolver(x => x.Version),
ResolvedType = new NonNullGraphType(new IntGraphType()), ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.Version),
Description = "The version of the asset." Description = "The version of the asset."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "created", Name = "created",
Resolver = Resolver(x => x.Created.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()), ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.Created.ToDateTimeUtc()),
Description = "The date and time when the asset has been created." Description = "The date and time when the asset has been created."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "createdBy", Name = "createdBy",
Resolver = Resolver(x => x.CreatedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.CreatedBy.ToString()),
Description = "The user that has created the asset." Description = "The user that has created the asset."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "lastModified", Name = "lastModified",
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()), ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()),
Description = "The date and time when the asset has been modified last." Description = "The date and time when the asset has been modified last."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "lastModifiedBy", Name = "lastModifiedBy",
Resolver = Resolver(x => x.LastModifiedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.LastModifiedBy.ToString()),
Description = "The user that has updated the asset last." Description = "The user that has updated the asset last."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "mimeType", Name = "mimeType",
Resolver = Resolver(x => x.MimeType),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.MimeType),
Description = "The mime type." Description = "The mime type."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "url", Name = "url",
Resolver = context.ResolveAssetUrl(),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = model.ResolveAssetUrl(),
Description = "The url to the asset." Description = "The url to the asset."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "thumbnailUrl", Name = "thumbnailUrl",
Resolver = context.ResolveAssetThumbnailUrl(),
ResolvedType = new StringGraphType(), ResolvedType = new StringGraphType(),
Resolver = model.ResolveAssetThumbnailUrl(),
Description = "The thumbnail url to the asset." Description = "The thumbnail url to the asset."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileName", Name = "fileName",
Resolver = Resolver(x => x.FileName),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.FileName),
Description = "The file name." Description = "The file name."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileType", Name = "fileType",
Resolver = Resolver(x => x.FileName.FileType()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type." Description = "The file type."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileSize", Name = "fileSize",
Resolver = Resolver(x => x.FileSize),
ResolvedType = new NonNullGraphType(new IntGraphType()), ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes." Description = "The size of the file in bytes."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "fileVersion", Name = "fileVersion",
Resolver = Resolver(x => x.FileVersion),
ResolvedType = new NonNullGraphType(new IntGraphType()), ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file." Description = "The version of the file."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "isImage", Name = "isImage",
Resolver = Resolver(x => x.IsImage),
ResolvedType = new NonNullGraphType(new BooleanGraphType()), ResolvedType = new NonNullGraphType(new BooleanGraphType()),
Resolver = Resolve(x => x.IsImage),
Description = "Determines of the created file is an image." Description = "Determines of the created file is an image."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "pixelWidth", Name = "pixelWidth",
Resolver = Resolver(x => x.PixelWidth),
ResolvedType = new IntGraphType(), ResolvedType = new IntGraphType(),
Resolver = Resolve(x => x.PixelWidth),
Description = "The width of the image in pixels if the asset is an image." Description = "The width of the image in pixels if the asset is an image."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "pixelHeight", Name = "pixelHeight",
Resolver = Resolver(x => x.PixelHeight),
ResolvedType = new IntGraphType(), ResolvedType = new IntGraphType(),
Resolver = Resolve(x => x.PixelHeight),
Description = "The height of the image in pixels if the asset is an image." Description = "The height of the image in pixels if the asset is an image."
}); });
if (context.CanGenerateAssetSourceUrl) if (model.CanGenerateAssetSourceUrl)
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = "sourceUrl", Name = "sourceUrl",
Resolver = context.ResolveAssetSourceUrl(),
ResolvedType = new StringGraphType(), ResolvedType = new StringGraphType(),
Resolver = model.ResolveAssetSourceUrl(),
Description = "The source url of the asset." Description = "The source url of the asset."
}); });
} }
@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "An asset"; Description = "An asset";
} }
private static IFieldResolver Resolver(Func<IAssetEntity, object> action) private static IFieldResolver Resolve(Func<IAssetEntity, object> action)
{ {
return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source)); return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source));
} }

14
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResultGraphType.cs → src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetsResultGraphType.cs

@ -13,30 +13,32 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class AssetResultGraphType : ObjectGraphType<IResultList<IAssetEntity>> public sealed class AssetsResultGraphType : ObjectGraphType<IResultList<IAssetEntity>>
{ {
public AssetResultGraphType(IGraphType assetType) public AssetsResultGraphType(IComplexGraphType assetType)
{ {
Name = $"AssetResultDto"; Name = $"AssetResultDto";
AddField(new FieldType AddField(new FieldType
{ {
Name = "total", Name = "total",
Resolver = Resolver(x => x.Total), Resolver = Resolve(x => x.Total),
ResolvedType = new NonNullGraphType(new IntGraphType()), ResolvedType = new NonNullGraphType(new IntGraphType()),
Description = $"The total number of asset." Description = $"The total count of assets."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "items", Name = "items",
Resolver = Resolver(x => x), Resolver = Resolve(x => x),
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)), ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
Description = $"The assets." Description = $"The assets."
}); });
Description = "List of assets and total count of assets.";
} }
private static IFieldResolver Resolver(Func<IResultList<IAssetEntity>, object> action) private static IFieldResolver Resolve(Func<IResultList<IAssetEntity>, object> action)
{ {
return new FuncFieldResolver<IResultList<IAssetEntity>, object>(c => action(c.Source)); return new FuncFieldResolver<IResultList<IAssetEntity>, object>(c => action(c.Source));
} }

44
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/CommandVersionGraphType.cs

@ -0,0 +1,44 @@
// ==========================================================================
// 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 CommandVersionGraphType : ComplexGraphType<CommandContext>
{
public CommandVersionGraphType()
{
Name = "CommandVersionDto";
AddField(new FieldType
{
Name = "version",
ResolvedType = new IntGraphType(),
Resolver = ResolveEtag(),
Description = "The new version of the item."
});
Description = "The result of a mutation";
}
private static IFieldResolver ResolveEtag()
{
return new FuncFieldResolver<CommandContext, int?>(x =>
{
if (x.Source.Result<object>() is EntitySavedResult result)
{
return (int)result.Version;
}
return null;
});
}
}
}

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

@ -0,0 +1,73 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
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,
ResolvedType = inputType,
Resolver = null,
Description = field.RawProperties.Hints
});
}
fieldGraphType.Description = $"The input structure of the {fieldName} of a {schemaName} content type.";
var fieldResolver = new FuncFieldResolver<NamedContentData, ContentFieldData>(c => c.Source.GetOrDefault(field.Name));
AddField(new FieldType
{
Name = field.Name.ToCamelCase(),
Resolver = fieldResolver,
ResolvedType = fieldGraphType
});
}
}
Description = $"The structure of a {schemaName} content type.";
}
}
}

17
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs

@ -9,22 +9,23 @@ using System.Linq;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData> public sealed class ContentDataGraphType : ObjectGraphType<NamedContentData>
{ {
public ContentDataGraphType(Schema schema, IGraphQLContext qlContext) public void Initialize(IGraphModel model, ISchemaEntity schema)
{ {
var schemaName = schema.Properties.Label.WithFallback(schema.Name); var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
Name = $"{schema.Name.ToPascalCase()}DataDto"; Name = $"{schemaType}DataDto";
foreach (var field in schema.Fields.Where(x => !x.IsHidden)) foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden))
{ {
var fieldInfo = qlContext.GetGraphType(field); var fieldInfo = model.GetGraphType(field);
if (fieldInfo.ResolveType != null) if (fieldInfo.ResolveType != null)
{ {
@ -32,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
var fieldGraphType = new ObjectGraphType var fieldGraphType = new ObjectGraphType
{ {
Name = $"{schema.Name.ToPascalCase()}Data{field.Name.ToPascalCase()}Dto" Name = $"{schemaType}Data{field.Name.ToPascalCase()}Dto"
}; };
var partition = qlContext.ResolvePartition(field.Partitioning); var partition = model.ResolvePartition(field.Partitioning);
foreach (var partitionItem in partition) foreach (var partitionItem in partition)
{ {

41
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs

@ -10,92 +10,81 @@ using System.Linq;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class ContentGraphType : ObjectGraphType<IContentEntity> public sealed class ContentGraphType : ObjectGraphType<IContentEntity>
{ {
private readonly ISchemaEntity schema; public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType)
private readonly IGraphQLContext ctx;
public ContentGraphType(ISchemaEntity schema, IGraphQLContext ctx)
{ {
this.ctx = ctx; var schemaType = schema.TypeName();
this.schema = schema; var schemaName = schema.DisplayName();
Name = $"{schema.Name.ToPascalCase()}Dto"; Name = $"{schemaType}Dto";
}
public void Initialize()
{
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.Name);
AddField(new FieldType AddField(new FieldType
{ {
Name = "id", Name = "id",
Resolver = Resolver(x => x.Id.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.Id.ToString()),
Description = $"The id of the {schemaName} content." Description = $"The id of the {schemaName} content."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "version", Name = "version",
Resolver = Resolver(x => x.Version),
ResolvedType = new NonNullGraphType(new IntGraphType()), ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.Version),
Description = $"The version of the {schemaName} content." Description = $"The version of the {schemaName} content."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "created", Name = "created",
Resolver = Resolver(x => x.Created.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()), ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.Created.ToDateTimeUtc()),
Description = $"The date and time when the {schemaName} content has been created." Description = $"The date and time when the {schemaName} content has been created."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "createdBy", Name = "createdBy",
Resolver = Resolver(x => x.CreatedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.CreatedBy.ToString()),
Description = $"The user that has created the {schemaName} content." Description = $"The user that has created the {schemaName} content."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "lastModified", Name = "lastModified",
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()), ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()),
Description = $"The date and time when the {schemaName} content has been modified last." Description = $"The date and time when the {schemaName} content has been modified last."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "lastModifiedBy", Name = "lastModifiedBy",
Resolver = Resolver(x => x.LastModifiedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.LastModifiedBy.ToString()),
Description = $"The user that has updated the {schemaName} content last." Description = $"The user that has updated the {schemaName} content last."
}); });
AddField(new FieldType AddField(new FieldType
{ {
Name = "url", Name = "url",
Resolver = ctx.ResolveContentUrl(schema),
ResolvedType = new NonNullGraphType(new StringGraphType()), ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = model.ResolveContentUrl(schema),
Description = $"The url to the the {schemaName} content." Description = $"The url to the the {schemaName} content."
}); });
var dataType = new ContentDataGraphType(schema.SchemaDef, ctx); if (contentDataType.Fields.Any())
if (dataType.Fields.Any())
{ {
AddField(new FieldType AddField(new FieldType
{ {
Name = "data", Name = "data",
Resolver = Resolver(x => x.Data), ResolvedType = new NonNullGraphType(contentDataType),
ResolvedType = new NonNullGraphType(dataType), Resolver = Resolve(x => x.Data),
Description = $"The data of the {schemaName} content." Description = $"The data of the {schemaName} content."
}); });
} }
@ -103,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The structure of a {schemaName} content type."; Description = $"The structure of a {schemaName} content type.";
} }
private static IFieldResolver Resolver(Func<IContentEntity, object> action) private static IFieldResolver Resolve(Func<IContentEntity, object> action)
{ {
return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source)); return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source));
} }

13
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentResultGraphType.cs → src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentsResultGraphType.cs

@ -8,18 +8,15 @@
using System; using System;
using GraphQL.Resolvers; using GraphQL.Resolvers;
using GraphQL.Types; using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
public sealed class ContentResultGraphType : ObjectGraphType<IResultList<IContentEntity>> public sealed class ContentsResultGraphType : ObjectGraphType<IResultList<IContentEntity>>
{ {
public ContentResultGraphType(IGraphQLContext ctx, ISchemaEntity schema, string schemaName) public ContentsResultGraphType(string schemaType, string schemaName, IComplexGraphType contentType)
{ {
Name = $"{schema.Name.ToPascalCase()}ResultDto"; Name = $"{schemaType}ResultDto";
var schemaType = ctx.GetSchemaType(schema.Id);
AddField(new FieldType AddField(new FieldType
{ {
@ -33,9 +30,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{ {
Name = "items", Name = "items",
Resolver = Resolver(x => x), Resolver = Resolver(x => x),
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)), ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Description = $"The {schemaName} items." Description = $"The {schemaName} items."
}); });
Description = $"List of {schemaName} items and total count.";
} }
private static IFieldResolver Resolver(Func<IResultList<IContentEntity>, object> action) private static IFieldResolver Resolver(Func<IResultList<IContentEntity>, object> action)

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 = new NonNullGraphType(new FloatGraphType())
});
AddField(new FieldType
{
Name = "longitude",
ResolvedType = new NonNullGraphType(new FloatGraphType())
});
}
}
}

55
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/GuidGraphType.cs

@ -0,0 +1,55 @@
// ==========================================================================
// 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
{
public sealed class GuidGraphType : ScalarGraphType
{
public GuidGraphType()
{
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;
}
}
}

41
src/Squidex.Domain.Apps.Entities/Schemas/SchemaExtensions.cs

@ -0,0 +1,41 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public static class SchemaExtensions
{
public static NamedId<Guid> NamedId(this ISchemaEntity schema)
{
return new NamedId<Guid>(schema.Id, schema.Name);
}
public static string TypeName(this ISchemaEntity schema)
{
return schema.SchemaDef.Name.ToPascalCase();
}
public static string DisplayName(this ISchemaEntity schema)
{
return schema.SchemaDef.Properties.Label.WithFallback(schema.TypeName());
}
public static string TypeName(this Schema schema)
{
return schema.Name.ToPascalCase();
}
public static string DisplayName(this Schema schema)
{
return schema.Properties.Label.WithFallback(schema.TypeName());
}
}
}

3
src/Squidex.Domain.Apps.Entities/SquidexCommand.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Security.Claims;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -14,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities
{ {
public RefToken Actor { get; set; } public RefToken Actor { get; set; }
public ClaimsPrincipal User { get; set; }
public long ExpectedVersion { get; set; } public long ExpectedVersion { get; set; }
} }
} }

1
src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;

8
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -60,9 +60,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </summary> /// </summary>
/// <param name="app">The name of the app.</param> /// <param name="app">The name of the app.</param>
/// <param name="ids">The optional asset ids.</param> /// <param name="ids">The optional asset ids.</param>
/// <param name="skip">The number of assets to skip.</param> /// <param name="skip">Optional number of assets to skip.</param>
/// <param name="take">The number of assets to take (Default: 20).</param> /// <param name="take">Optional number of assets to take (Default: 20).</param>
/// <param name="query">The query to limit the files by name.</param> /// <param name="query">Optional query to limit the files by name.</param>
/// <param name="mimeTypes">Comma separated list of mime types to get.</param> /// <param name="mimeTypes">Comma separated list of mime types to get.</param>
/// <returns> /// <returns>
/// 200 => Assets returned. /// 200 => Assets returned.
@ -76,7 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/")] [Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetsDto), 200)] [ProducesResponseType(typeof(AssetsDto), 200)]
[ApiCosts(1)] [ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] string ids = null, [FromQuery] int skip = 0, [FromQuery] int take = 10) public async Task<IActionResult> GetAssets(string app, [FromQuery] string query = null, [FromQuery] string mimeTypes = null, [FromQuery] string ids = null, [FromQuery] int skip = 0, [FromQuery] int take = 20)
{ {
var mimeTypeList = new HashSet<string>(); var mimeTypeList = new HashSet<string>();

18
src/Squidex/Areas/Api/Controllers/Content/ContentsController.cs

@ -90,7 +90,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
await contentQuery.QueryAsync(App, name, User, archived, idsList) : await contentQuery.QueryAsync(App, name, User, archived, idsList) :
await contentQuery.QueryAsync(App, name, User, archived, Request.QueryString.ToString()); await contentQuery.QueryAsync(App, name, User, archived, Request.QueryString.ToString());
var response = new AssetsDto var response = new ContentsDto
{ {
Total = result.Contents.Total, Total = result.Contents.Total,
Items = result.Contents.Take(200).Select(item => Items = result.Contents.Take(200).Select(item =>
@ -161,7 +161,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new CreateContent { ContentId = Guid.NewGuid(), User = User, Data = request.ToCleaned(), Publish = publish }; var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned(), Publish = publish };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -179,7 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new UpdateContent { ContentId = id, User = User, Data = request.ToCleaned() }; var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -197,7 +197,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new PatchContent { ContentId = id, User = User, Data = request.ToCleaned() }; var command = new PatchContent { ContentId = id, Data = request.ToCleaned() };
var context = await CommandBus.PublishAsync(command); var context = await CommandBus.PublishAsync(command);
@ -215,7 +215,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new ChangeContentStatus { Status = Status.Published, ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Published, ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -230,7 +230,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -245,7 +245,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Archived, ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -260,7 +260,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id, User = User }; var command = new ChangeContentStatus { Status = Status.Draft, ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);
@ -275,7 +275,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{ {
await contentQuery.FindSchemaAsync(App, name); await contentQuery.FindSchemaAsync(App, name);
var command = new DeleteContent { ContentId = id, User = User }; var command = new DeleteContent { ContentId = id };
await CommandBus.PublishAsync(command); await CommandBus.PublishAsync(command);

35
src/Squidex/Areas/Api/Controllers/Content/Generator/SchemaSwaggerGenerator.cs

@ -14,6 +14,7 @@ using Squidex.Config;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger; using Squidex.Pipeline.Swagger;
using Squidex.Shared.Identity; using Squidex.Shared.Identity;
@ -32,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private readonly JsonSchema4 dataSchema; private readonly JsonSchema4 dataSchema;
private readonly string schemaPath; private readonly string schemaPath;
private readonly string schemaName; private readonly string schemaName;
private readonly string schemaKey; private readonly string schemaType;
private readonly string appPath; private readonly string appPath;
static SchemaSwaggerGenerator() static SchemaSwaggerGenerator()
@ -68,12 +69,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
appPath = path; appPath = path;
schemaPath = schema.Name; schemaPath = schema.Name;
schemaName = schema.Properties.Label.WithFallback(schema.Name); schemaName = schema.DisplayName();
schemaKey = schema.Name.ToPascalCase(); schemaType = schema.TypeName();
dataSchema = schemaResolver($"{schemaKey}Dto", schema.BuildJsonSchema(partitionResolver, schemaResolver)); dataSchema = schemaResolver($"{schemaType}Dto", schema.BuildJsonSchema(partitionResolver, schemaResolver));
contentSchema = schemaResolver($"{schemaKey}ContentDto", schemaBuilder.CreateContentSchema(schema, dataSchema)); contentSchema = schemaResolver($"{schemaType}ContentDto", schemaBuilder.CreateContentSchema(schema, dataSchema));
} }
public void GenerateSchemaOperations() public void GenerateSchemaOperations()
@ -108,13 +109,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation => return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation =>
{ {
operation.OperationId = $"Query{schemaKey}Contents"; operation.OperationId = $"Query{schemaType}Contents";
operation.Summary = $"Queries {schemaName} contents."; operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity; operation.Security = ReaderSecurity;
operation.Description = SchemaQueryDescription; operation.Description = SchemaQueryDescription;
operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take."); operation.AddQueryParameter("$top", JsonObjectType.Number, "Optional number of contents to take (Default: 20).");
operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip."); operation.AddQueryParameter("$skip", JsonObjectType.Number, "Optional number of contents to skip.");
operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter."); operation.AddQueryParameter("$filter", JsonObjectType.String, "Optional OData filter.");
operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search."); operation.AddQueryParameter("$search", JsonObjectType.String, "Optional OData full text search.");
@ -128,7 +129,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{ {
operation.OperationId = $"Get{schemaKey}Content"; operation.OperationId = $"Get{schemaType}Content";
operation.Summary = $"Get a {schemaName} content."; operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity; operation.Security = ReaderSecurity;
@ -140,7 +141,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Post, null, $"{appPath}/{schemaPath}", operation => return AddOperation(SwaggerOperationMethod.Post, null, $"{appPath}/{schemaPath}", operation =>
{ {
operation.OperationId = $"Create{schemaKey}Content"; operation.OperationId = $"Create{schemaType}Content";
operation.Summary = $"Create a {schemaName} content."; operation.Summary = $"Create a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -155,7 +156,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{ {
operation.OperationId = $"Update{schemaKey}Content"; operation.OperationId = $"Update{schemaType}Content";
operation.Summary = $"Update a {schemaName} content."; operation.Summary = $"Update a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -169,8 +170,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation => return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{ {
operation.OperationId = $"Path{schemaKey}Content"; operation.OperationId = $"Path{schemaType}Content";
operation.Summary = $"Patchs a {schemaName} content."; operation.Summary = $"Patch a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription); operation.AddBodyParameter("data", dataSchema, SchemaBodyDescription);
@ -183,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/publish", operation => return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/publish", operation =>
{ {
operation.OperationId = $"Publish{schemaKey}Content"; operation.OperationId = $"Publish{schemaType}Content";
operation.Summary = $"Publish a {schemaName} content."; operation.Summary = $"Publish a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -195,7 +196,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/unpublish", operation => return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/unpublish", operation =>
{ {
operation.OperationId = $"Unpublish{schemaKey}Content"; operation.OperationId = $"Unpublish{schemaType}Content";
operation.Summary = $"Unpublish a {schemaName} content."; operation.Summary = $"Unpublish a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -207,7 +208,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation => return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/archive", operation =>
{ {
operation.OperationId = $"Archive{schemaKey}Content"; operation.OperationId = $"Archive{schemaType}Content";
operation.Summary = $"Archive a {schemaName} content."; operation.Summary = $"Archive a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -219,7 +220,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation => return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appPath}/{schemaPath}/{{id}}/restore", operation =>
{ {
operation.OperationId = $"Restore{schemaKey}Content"; operation.OperationId = $"Restore{schemaType}Content";
operation.Summary = $"Restore a {schemaName} content."; operation.Summary = $"Restore a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;
@ -231,7 +232,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{ {
return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appPath}/{schemaPath}/{{id}}/", operation => return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appPath}/{schemaPath}/{{id}}/", operation =>
{ {
operation.OperationId = $"Delete{schemaKey}Content"; operation.OperationId = $"Delete{schemaType}Content";
operation.Summary = $"Delete a {schemaName} content."; operation.Summary = $"Delete a {schemaName} content.";
operation.Security = EditorSecurity; operation.Security = EditorSecurity;

2
src/Squidex/Areas/Api/Controllers/Content/Models/AssetsDto.cs → src/Squidex/Areas/Api/Controllers/Content/Models/ContentsDto.cs

@ -7,7 +7,7 @@
namespace Squidex.Areas.Api.Controllers.Contents.Models namespace Squidex.Areas.Api.Controllers.Contents.Models
{ {
public sealed class AssetsDto public sealed class ContentsDto
{ {
/// <summary> /// <summary>
/// The total number of content items. /// The total number of content items.

18
src/Squidex/Pipeline/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs

@ -27,13 +27,21 @@ namespace Squidex.Pipeline.CommandMiddlewares
public Task HandleAsync(CommandContext context, Func<Task> next) public Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (context.Command is SquidexCommand squidexCommand && squidexCommand.Actor == null) if (context.Command is SquidexCommand squidexCommand)
{ {
var actorToken = if (squidexCommand.Actor == null)
FindActorFromSubject() ?? {
FindActorFromClient(); var actorToken =
FindActorFromSubject() ??
FindActorFromClient();
squidexCommand.Actor = actorToken ?? throw new SecurityException("No actor with subject or client id available."); squidexCommand.Actor = actorToken ?? throw new SecurityException("No actor with subject or client id available.");
}
if (squidexCommand.User == null)
{
squidexCommand.User = httpContextAccessor.HttpContext.User;
}
} }
return next(); return next();

207
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -0,0 +1,207 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
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;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public class GraphQLMutationTests : GraphQLTestBase
{
private readonly CommandContext commandContext = new CommandContext(new PatchContent());
public GraphQLMutationTests()
{
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored))
.Returns(commandContext);
}
[Fact]
public async Task Should_return_single_content_when_patching_content()
{
var contentId = Guid.NewGuid();
var content = CreateContent(contentId, Guid.Empty, Guid.Empty);
var query = $@"
mutation OP($data: MySchemaInputDto!) {{
patchMySchemaContent(id: ""{contentId}"", data: $data) {{
myString {{
de
}}
myNumber {{
iv
}}
myBoolean {{
iv
}}
myDatetime {{
iv
}}
myJson {{
iv
}}
myGeolocation {{
iv
}}
myTags {{
iv
}}
}}
}}";
commandContext.Complete(new ContentDataChangedResult(content.Data, 1));
var camelContent = new NamedContentData();
foreach (var kvp in content.Data)
{
if (kvp.Key != "my-json")
{
camelContent[kvp.Key.ToCamelCase()] = kvp.Value;
}
}
var variables =
new JObject(
new JProperty("data", JObject.FromObject(camelContent)));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query, Variables = variables });
var expected = new
{
data = new
{
patchMySchemaContent = new
{
myString = new
{
de = "value"
},
myNumber = new
{
iv = 1
},
myBoolean = new
{
iv = true
},
myDatetime = new
{
iv = content.LastModified.ToDateTimeUtc()
},
myJson = new
{
iv = new
{
value = 1
}
},
myGeolocation = new
{
iv = new
{
latitude = 10,
longitude = 20
}
},
myTags = new
{
iv = new[]
{
"tag1",
"tag2"
}
}
}
}
};
AssertResult(expected, result);
}
[Fact]
public async Task Should_publish_command_for_restore()
{
var contentId = Guid.NewGuid();
var query = $@"
mutation {{
restoreMySchemaContent(id: ""{contentId}"") {{
version
}}
}}";
commandContext.Complete(new EntitySavedResult(13));
var result = await sut.QueryAsync(app, user, new GraphQLQuery { Query = query });
var expected = new
{
data = new
{
restoreMySchemaContent = new
{
version = 13
}
}
};
AssertResult(expected, result);
A.CallTo(() => commandBus.PublishAsync(
A<ChangeContentStatus>.That.Matches(x =>
x.SchemaId.Equals(schema.NamedId()) &&
x.ContentId == contentId &&
x.Status == Status.Draft)))
.MustHaveHappened();
}
[Fact]
public async Task Should_publish_command_for_delete()
{
var contentId = Guid.NewGuid();
var query = $@"
mutation {{
deleteMySchemaContent(id: ""{contentId}"") {{
version
}}
}}";
commandContext.Complete(new EntitySavedResult(13));
var result = await sut.QueryAsync(app, user, 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.SchemaId.Equals(schema.NamedId()) &&
x.ContentId == contentId)))
.MustHaveHappened();
}
}
}

167
tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTests.cs → tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -7,87 +7,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime.Extensions;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Xunit; using Xunit;
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
public class GraphQLTests public class GraphQLQueriesTests : GraphQLTestBase
{ {
private static readonly Guid schemaId = Guid.NewGuid();
private static readonly Guid appId = Guid.NewGuid();
private static readonly string appName = "my-app";
private readonly Schema schemaDef;
private readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IAppEntity app = A.Dummy<IAppEntity>();
private readonly ClaimsPrincipal user = new ClaimsPrincipal();
private readonly IGraphQLService sut;
public GraphQLTests()
{
schemaDef =
new Schema("my-schema")
.AddField(new JsonField(1, "my-json", Partitioning.Invariant,
new JsonFieldProperties()))
.AddField(new StringField(2, "my-string", Partitioning.Language,
new StringFieldProperties()))
.AddField(new NumberField(3, "my-number", Partitioning.Invariant,
new NumberFieldProperties()))
.AddField(new AssetsField(4, "my-assets", Partitioning.Invariant,
new AssetsFieldProperties()))
.AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant,
new BooleanFieldProperties()))
.AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant,
new DateTimeFieldProperties()))
.AddField(new ReferencesField(7, "my-references", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId }))
.AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }))
.AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant,
new GeolocationFieldProperties()))
.AddField(new TagsField(11, "my-tags", Partitioning.Invariant,
new TagsFieldProperties()));
A.CallTo(() => app.Id).Returns(appId);
A.CallTo(() => app.Name).Returns(appName);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE));
A.CallTo(() => schema.Id).Returns(schemaId);
A.CallTo(() => schema.Name).Returns(schemaDef.Name);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
A.CallTo(() => schema.IsPublished).Returns(true);
A.CallTo(() => schema.ScriptQuery).Returns("<script-query>");
var allSchemas = new List<ISchemaEntity> { schema };
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetRepository, contentQuery, new FakeUrlGenerator());
}
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
@ -103,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -111,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
const string query = @" const string query = @"
query { query {
queryAssets(search: ""my-query"", top: 30, skip: 5) { queryAssets(search: ""my-query"", take: 30, skip: 5) {
id id
version version
created created
@ -169,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -177,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{ {
const string query = @" const string query = @"
query { query {
queryAssetsWithTotal(search: ""my-query"", top: 30, skip: 5) { queryAssetsWithTotal(search: ""my-query"", take: 30, skip: 5) {
total total
items { items {
id id
@ -242,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -304,7 +234,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -417,7 +347,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -537,7 +467,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -646,7 +576,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -706,7 +636,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -766,7 +696,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
} }
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result);
} }
[Fact] [Fact]
@ -803,78 +733,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
data = (object)null data = (object)null
}; };
AssertJson(expected, new { data = result.Data }); AssertResult(expected, result, false);
}
private static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null)
{
var now = DateTime.UtcNow.ToInstant();
data = data ??
new NamedContentData()
.AddField("my-json",
new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 })))
.AddField("my-string",
new ContentFieldData().AddValue("de", "value"))
.AddField("my-assets",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId })))
.AddField("my-number",
new ContentFieldData().AddValue("iv", 1))
.AddField("my-boolean",
new ContentFieldData().AddValue("iv", true))
.AddField("my-datetime",
new ContentFieldData().AddValue("iv", now.ToDateTimeUtc()))
.AddField("my-tags",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" })))
.AddField("my-references",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId })))
.AddField("my-geolocation",
new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 })));
var content = new FakeContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken("subject", "user1"),
LastModified = now,
LastModifiedBy = new RefToken("subject", "user2"),
Data = data
};
return content;
}
private static IAssetEntity CreateAsset(Guid id)
{
var now = DateTime.UtcNow.ToInstant();
var asset = new FakeAssetEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken("subject", "user1"),
LastModified = now,
LastModifiedBy = new RefToken("subject", "user2"),
FileName = "MyFile.png",
FileSize = 1024,
FileVersion = 123,
MimeType = "image/png",
IsImage = true,
PixelWidth = 800,
PixelHeight = 600
};
return asset;
}
private static void AssertJson(object expected, object result)
{
var resultJson = JsonConvert.SerializeObject(result, Formatting.Indented);
var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented);
Assert.Equal(expectJson, resultJson);
} }
} }
} }

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

@ -0,0 +1,169 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Security.Claims;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NodaTime.Extensions;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.TestData;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Xunit;
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter
#pragma warning disable SA1401 // Fields must be private
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public class GraphQLTestBase
{
protected static readonly Guid schemaId = Guid.NewGuid();
protected static readonly Guid appId = Guid.NewGuid();
protected static readonly string appName = "my-app";
protected readonly Schema schemaDef;
protected readonly IContentQueryService contentQuery = A.Fake<IContentQueryService>();
protected readonly ICommandBus commandBus = A.Fake<ICommandBus>();
protected readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
protected readonly ISchemaEntity schema = A.Fake<ISchemaEntity>();
protected readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
protected readonly IAppProvider appProvider = A.Fake<IAppProvider>();
protected readonly IAppEntity app = A.Dummy<IAppEntity>();
protected readonly ClaimsPrincipal user = new ClaimsPrincipal();
protected readonly IGraphQLService sut;
public GraphQLTestBase()
{
schemaDef =
new Schema("my-schema")
.AddField(new JsonField(1, "my-json", Partitioning.Invariant,
new JsonFieldProperties()))
.AddField(new StringField(2, "my-string", Partitioning.Language,
new StringFieldProperties()))
.AddField(new NumberField(3, "my-number", Partitioning.Invariant,
new NumberFieldProperties()))
.AddField(new AssetsField(4, "my-assets", Partitioning.Invariant,
new AssetsFieldProperties()))
.AddField(new BooleanField(5, "my-boolean", Partitioning.Invariant,
new BooleanFieldProperties()))
.AddField(new DateTimeField(6, "my-datetime", Partitioning.Invariant,
new DateTimeFieldProperties()))
.AddField(new ReferencesField(7, "my-references", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = schemaId }))
.AddField(new ReferencesField(9, "my-invalid", Partitioning.Invariant,
new ReferencesFieldProperties { SchemaId = Guid.NewGuid() }))
.AddField(new GeolocationField(10, "my-geolocation", Partitioning.Invariant,
new GeolocationFieldProperties()))
.AddField(new TagsField(11, "my-tags", Partitioning.Invariant,
new TagsFieldProperties()));
A.CallTo(() => app.Id).Returns(appId);
A.CallTo(() => app.Name).Returns(appName);
A.CallTo(() => app.LanguagesConfig).Returns(LanguagesConfig.Build(Language.DE));
A.CallTo(() => schema.Id).Returns(schemaId);
A.CallTo(() => schema.Name).Returns(schemaDef.Name);
A.CallTo(() => schema.SchemaDef).Returns(schemaDef);
A.CallTo(() => schema.IsPublished).Returns(true);
A.CallTo(() => schema.ScriptQuery).Returns("<script-query>");
var allSchemas = new List<ISchemaEntity> { schema };
A.CallTo(() => appProvider.GetSchemasAsync(appId)).Returns(allSchemas);
sut = new CachingGraphQLService(cache, appProvider, assetRepository, commandBus, contentQuery, new FakeUrlGenerator());
}
protected static IContentEntity CreateContent(Guid id, Guid refId, Guid assetId, NamedContentData data = null)
{
var now = DateTime.UtcNow.ToInstant();
data = data ??
new NamedContentData()
.AddField("my-json",
new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 })))
.AddField("my-string",
new ContentFieldData().AddValue("de", "value"))
.AddField("my-assets",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId })))
.AddField("my-number",
new ContentFieldData().AddValue("iv", 1))
.AddField("my-boolean",
new ContentFieldData().AddValue("iv", true))
.AddField("my-datetime",
new ContentFieldData().AddValue("iv", now.ToDateTimeUtc()))
.AddField("my-tags",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" })))
.AddField("my-references",
new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId })))
.AddField("my-geolocation",
new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 })));
var content = new ContentEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken("subject", "user1"),
LastModified = now,
LastModifiedBy = new RefToken("subject", "user2"),
Data = data
};
return content;
}
protected static IAssetEntity CreateAsset(Guid id)
{
var now = DateTime.UtcNow.ToInstant();
var asset = new FakeAssetEntity
{
Id = id,
Version = 1,
Created = now,
CreatedBy = new RefToken("subject", "user1"),
LastModified = now,
LastModifiedBy = new RefToken("subject", "user2"),
FileName = "MyFile.png",
FileSize = 1024,
FileVersion = 123,
MimeType = "image/png",
IsImage = true,
PixelWidth = 800,
PixelHeight = 600
};
return asset;
}
protected static void AssertResult(object expected, (object Data, object[] Errors) result, bool checkErrors = true)
{
if (checkErrors && (result.Errors != null && result.Errors.Length > 0))
{
throw new InvalidOperationException(result.Errors[0]?.ToString());
}
var resultJson = JsonConvert.SerializeObject(new { data = result.Data }, Formatting.Indented);
var expectJson = JsonConvert.SerializeObject(expected, Formatting.Indented);
Assert.Equal(expectJson, resultJson);
}
}
}

35
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeContentEntity.cs

@ -1,35 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.TestData
{
public sealed class FakeContentEntity : IContentEntity
{
public Guid Id { get; set; }
public Guid AppId { get; set; }
public long Version { get; set; }
public Instant Created { get; set; }
public Instant LastModified { get; set; }
public RefToken CreatedBy { get; set; }
public RefToken LastModifiedBy { get; set; }
public NamedContentData Data { get; set; }
public Status Status { get; set; }
}
}
Loading…
Cancel
Save