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.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;

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

@ -6,15 +6,12 @@
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public abstract class ContentCommand : SchemaCommand, IAggregateCommand
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
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 Microsoft.OData;
using Microsoft.OData.UriParser;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
@ -122,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var content in contents)
{
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;
@ -199,23 +198,5 @@ namespace Squidex.Domain.Apps.Entities.Contents
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.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
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 readonly IContentQueryService contentQuery;
private readonly ICommandBus commandBus;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IAppProvider appProvider;
@ -27,17 +29,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider,
IAssetRepository assetRepository,
ICommandBus commandBus,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentQuery, nameof(urlGenerator));
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetRepository = assetRepository;
this.commandBus = commandBus;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
}
@ -53,9 +58,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
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)

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.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure.Commands;
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 GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user,
public GraphQLExecutionContext(IAppEntity app, IAssetRepository assetRepository, ICommandBus commandBus, IContentQueryService contentQuery, ClaimsPrincipal user,
IGraphQLUrlGenerator urlGenerator)
: base(app, assetRepository, contentQuery, user)
{
CommandBus = commandBus;
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
{
public sealed class GraphQLModel : IGraphQLContext
public sealed class GraphQLModel : IGraphModel
{
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 PartitionResolver partitionResolver;
private readonly IAppEntity app;
private readonly IGraphType assetType;
private readonly IGraphType assetListType;
private readonly IComplexGraphType assetType;
private readonly GraphQLSchema graphQLSchema;
public bool CanGenerateAssetSourceUrl { get; }
@ -48,6 +50,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this);
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)>>
{
{
@ -70,14 +108,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
typeof(JsonField),
field => ResolveDefault("Json")
},
{
typeof(TagsField),
field => ResolveDefault("String")
},
{
typeof(GeolocationField),
field => ResolveDefault("Geolocation")
},
{
typeof(TagsField),
field => ResolveDefault("String")
},
{
typeof(AssetsField),
field => ResolveAssets(assetListType)
@ -90,11 +128,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
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 context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
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 context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
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 context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
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 context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
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 context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedAssetsAsync(contentIds);
@ -167,27 +213,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private ValueTuple<IGraphType, IFieldResolver> ResolveReferences(Field field)
{
var schemaId = ((ReferencesField)field).Properties.SchemaId;
var schemaType = GetSchemaType(schemaId);
if (schemaType == null)
var contentType = GetContentType(schemaId);
if (contentType == null)
{
return (null, null);
}
var resolver = new FuncFieldResolver<ContentFieldData, object>(c =>
{
var context = (GraphQLQueryContext)c.UserContext;
var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedContentsAsync(schemaId, contentIds);
});
var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId)));
var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));
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));
@ -208,7 +255,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key);
}
public IGraphType GetAssetType()
public IComplexGraphType GetAssetType()
{
return assetType;
}
@ -218,11 +265,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return fieldInfos[field.GetType()](field);
}
public IGraphType GetSchemaType(Guid schemaId)
public IComplexGraphType GetContentDataType(Guid 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
{
public interface IGraphQLContext
public interface IGraphModel
{
bool CanGenerateAssetSourceUrl { get; }
IFieldPartitioning ResolvePartition(Partitioning key);
IGraphType GetAssetType();
IComplexGraphType GetAssetType();
IGraphType GetSchemaType(Guid schemaId);
IComplexGraphType GetContentType(Guid schemaId);
IComplexGraphType GetContentDataType(Guid schemaId);
IFieldResolver ResolveAssetUrl();
@ -32,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IFieldResolver ResolveContentUrl(ISchemaEntity schema);
IGraphType GetInputGraphType(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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
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);
AddAssetsQueries(assetType);
foreach (var schema in schemas)
{
var schemaName = schema.SchemaDef.Properties.Label.WithFallback(schema.SchemaDef.Name);
var schemaType = ctx.GetSchemaType(schema.Id);
var schemaId = schema.Id;
var schemaType = schema.TypeName();
var schemaName = schema.DisplayName();
AddContentFind(schema, schemaType, schemaName);
AddContentQueries(ctx, schema, schemaType, schemaName);
var contentType = model.GetContentType(schema.Id);
AddContentFind(schemaId, schemaType, schemaName, contentType);
AddContentQueries(schemaId, schemaType, schemaName, contentType);
}
Description = "The app queries.";
@ -43,102 +46,94 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Name = "findAsset",
Arguments = CreateAssetFindArguments(),
ResolvedType = assetType,
Resolver = new FuncFieldResolver<object>(c =>
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString()));
var assetId = c.GetArgument<Guid>("id");
return context.FindAssetAsync(contentId);
return e.FindAssetAsync(assetId);
}),
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
{
Name = $"find{schema.Name.ToPascalCase()}Content",
Name = $"find{schemaType}Content",
Arguments = CreateContentFindTypes(schemaName),
ResolvedType = schemaType,
Resolver = new FuncFieldResolver<object>(c =>
ResolvedType = contentType,
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
var contentId = Guid.Parse(c.GetArgument("id", Guid.Empty.ToString()));
var contentId = c.GetArgument<Guid>("id");
return context.FindContentAsync(schema.Id, contentId);
return e.FindContentAsync(schemaId, contentId);
}),
Description = $"Find an {schemaName} content by id."
});
}
private void AddAssetsQueries(IGraphType assetType)
private void AddAssetsQueries(IComplexGraphType assetType)
{
AddField(new FieldType
{
Name = "queryAssets",
Arguments = CreateAssetQueryArguments(),
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
Resolver = new FuncFieldResolver<object>(c =>
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
var argTop = c.GetArgument("top", 20);
var argTake = c.GetArgument("take", 20);
var argSkip = c.GetArgument("skip", 0);
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
{
Name = "queryAssetsWithTotal",
Arguments = CreateAssetQueryArguments(),
ResolvedType = new AssetResultGraphType(assetType),
Resolver = new FuncFieldResolver<object>(c =>
ResolvedType = new AssetsResultGraphType(assetType),
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
var argTop = c.GetArgument("top", 20);
var argTake = c.GetArgument("take", 20);
var argSkip = c.GetArgument("skip", 0);
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
{
Name = $"query{schema.Name.ToPascalCase()}Contents",
Name = $"query{schemaType}Contents",
Arguments = CreateContentQueryArguments(),
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)),
Resolver = new FuncFieldResolver<object>(c =>
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
var contentQuery = BuildODataQuery(c);
return context.QueryContentsAsync(schema.Id.ToString(), contentQuery);
return e.QueryContentsAsync(schemaId.ToString(), contentQuery);
}),
Description = $"Query {schemaName} content items."
});
AddField(new FieldType
{
Name = $"query{schema.Name.ToPascalCase()}ContentsWithTotal",
Name = $"query{schemaType}ContentsWithTotal",
Arguments = CreateContentQueryArguments(),
ResolvedType = new ContentResultGraphType(ctx, schema, schemaName),
Resolver = new FuncFieldResolver<object>(c =>
ResolvedType = new ContentsResultGraphType(schemaType, schemaName, contentType),
Resolver = ResolveAsync((c, e) =>
{
var context = (GraphQLQueryContext)c.UserContext;
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."
});
@ -148,10 +143,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
return new QueryArguments
{
new QueryArgument(typeof(StringGraphType))
new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{
Name = "id",
Description = "The id of the asset.",
Description = "The id of the asset (GUID).",
DefaultValue = string.Empty
}
};
@ -161,10 +156,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
return new QueryArguments
{
new QueryArgument(typeof(StringGraphType))
new QueryArgument(typeof(NonNullGraphType<GuidGraphType>))
{
Name = "id",
Description = $"The id of the {schemaName} content.",
Description = $"The id of the {schemaName} content (GUID)",
DefaultValue = string.Empty
}
};
@ -176,8 +171,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
new QueryArgument(typeof(IntGraphType))
{
Name = "top",
Description = "Optional number of assets to take.",
Name = "take",
Description = "Optional number of assets to take (Default: 20).",
DefaultValue = 20
},
new QueryArgument(typeof(IntGraphType))
@ -189,7 +184,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(typeof(StringGraphType))
{
Name = "search",
Description = "Optional query.",
Description = "Optional query to limit the files by name.",
DefaultValue = string.Empty
}
};
@ -202,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
new QueryArgument(typeof(IntGraphType))
{
Name = "top",
Description = "Optional number of contents to take.",
Description = "Optional number of contents to take (Default: 20).",
DefaultValue = 20
},
new QueryArgument(typeof(IntGraphType))
@ -242,5 +237,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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 AssetGraphType(IGraphQLContext context)
public AssetGraphType(IGraphModel model)
{
Name = "AssetDto";
AddField(new FieldType
{
Name = "id",
Resolver = Resolver(x => x.Id.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.Id.ToString()),
Description = "The id of the asset."
});
AddField(new FieldType
{
Name = "version",
Resolver = Resolver(x => x.Version),
ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.Version),
Description = "The version of the asset."
});
AddField(new FieldType
{
Name = "created",
Resolver = Resolver(x => x.Created.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.Created.ToDateTimeUtc()),
Description = "The date and time when the asset has been created."
});
AddField(new FieldType
{
Name = "createdBy",
Resolver = Resolver(x => x.CreatedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.CreatedBy.ToString()),
Description = "The user that has created the asset."
});
AddField(new FieldType
{
Name = "lastModified",
Resolver = Resolver(x => x.LastModified.ToDateTimeUtc()),
ResolvedType = new NonNullGraphType(new DateGraphType()),
Resolver = Resolve(x => x.LastModified.ToDateTimeUtc()),
Description = "The date and time when the asset has been modified last."
});
AddField(new FieldType
{
Name = "lastModifiedBy",
Resolver = Resolver(x => x.LastModifiedBy.ToString()),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.LastModifiedBy.ToString()),
Description = "The user that has updated the asset last."
});
AddField(new FieldType
{
Name = "mimeType",
Resolver = Resolver(x => x.MimeType),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.MimeType),
Description = "The mime type."
});
AddField(new FieldType
{
Name = "url",
Resolver = context.ResolveAssetUrl(),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = model.ResolveAssetUrl(),
Description = "The url to the asset."
});
AddField(new FieldType
{
Name = "thumbnailUrl",
Resolver = context.ResolveAssetThumbnailUrl(),
ResolvedType = new StringGraphType(),
Resolver = model.ResolveAssetThumbnailUrl(),
Description = "The thumbnail url to the asset."
});
AddField(new FieldType
{
Name = "fileName",
Resolver = Resolver(x => x.FileName),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.FileName),
Description = "The file name."
});
AddField(new FieldType
{
Name = "fileType",
Resolver = Resolver(x => x.FileName.FileType()),
ResolvedType = new NonNullGraphType(new StringGraphType()),
Resolver = Resolve(x => x.FileName.FileType()),
Description = "The file type."
});
AddField(new FieldType
{
Name = "fileSize",
Resolver = Resolver(x => x.FileSize),
ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.FileSize),
Description = "The size of the file in bytes."
});
AddField(new FieldType
{
Name = "fileVersion",
Resolver = Resolver(x => x.FileVersion),
ResolvedType = new NonNullGraphType(new IntGraphType()),
Resolver = Resolve(x => x.FileVersion),
Description = "The version of the file."
});
AddField(new FieldType
{
Name = "isImage",
Resolver = Resolver(x => x.IsImage),
ResolvedType = new NonNullGraphType(new BooleanGraphType()),
Resolver = Resolve(x => x.IsImage),
Description = "Determines of the created file is an image."
});
AddField(new FieldType
{
Name = "pixelWidth",
Resolver = Resolver(x => x.PixelWidth),
ResolvedType = new IntGraphType(),
Resolver = Resolve(x => x.PixelWidth),
Description = "The width of the image in pixels if the asset is an image."
});
AddField(new FieldType
{
Name = "pixelHeight",
Resolver = Resolver(x => x.PixelHeight),
ResolvedType = new IntGraphType(),
Resolver = Resolve(x => x.PixelHeight),
Description = "The height of the image in pixels if the asset is an image."
});
if (context.CanGenerateAssetSourceUrl)
if (model.CanGenerateAssetSourceUrl)
{
AddField(new FieldType
{
Name = "sourceUrl",
Resolver = context.ResolveAssetSourceUrl(),
ResolvedType = new StringGraphType(),
Resolver = model.ResolveAssetSourceUrl(),
Description = "The source url of the asset."
});
}
@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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));
}

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
{
public sealed class AssetResultGraphType : ObjectGraphType<IResultList<IAssetEntity>>
public sealed class AssetsResultGraphType : ObjectGraphType<IResultList<IAssetEntity>>
{
public AssetResultGraphType(IGraphType assetType)
public AssetsResultGraphType(IComplexGraphType assetType)
{
Name = $"AssetResultDto";
AddField(new FieldType
{
Name = "total",
Resolver = Resolver(x => x.Total),
Resolver = Resolve(x => x.Total),
ResolvedType = new NonNullGraphType(new IntGraphType()),
Description = $"The total number of asset."
Description = $"The total count of assets."
});
AddField(new FieldType
{
Name = "items",
Resolver = Resolver(x => x),
Resolver = Resolve(x => x),
ResolvedType = new ListGraphType(new NonNullGraphType(assetType)),
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));
}

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.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Schema = Squidex.Domain.Apps.Core.Schemas.Schema;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
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)
{
@ -32,10 +33,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
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)
{

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

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

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 GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
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";
var schemaType = ctx.GetSchemaType(schema.Id);
Name = $"{schemaType}ResultDto";
AddField(new FieldType
{
@ -33,9 +30,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "items",
Resolver = Resolver(x => x),
ResolvedType = new ListGraphType(new NonNullGraphType(schemaType)),
ResolvedType = new ListGraphType(new NonNullGraphType(contentType)),
Description = $"The {schemaName} items."
});
Description = $"List of {schemaName} items and total count.";
}
private static IFieldResolver Resolver(Func<IResultList<IContentEntity>, object> action)

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.
// ==========================================================================
using System.Security.Claims;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
@ -14,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities
{
public RefToken Actor { get; set; }
public ClaimsPrincipal User { get; set; }
public long ExpectedVersion { get; set; }
}
}

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

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

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

@ -60,9 +60,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="ids">The optional asset ids.</param>
/// <param name="skip">The number of assets to skip.</param>
/// <param name="take">The number of assets to take (Default: 20).</param>
/// <param name="query">The query to limit the files by name.</param>
/// <param name="skip">Optional number of assets to skip.</param>
/// <param name="take">Optional number of assets to take (Default: 20).</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>
/// <returns>
/// 200 => Assets returned.
@ -76,7 +76,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetsDto), 200)]
[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>();

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, Request.QueryString.ToString());
var response = new AssetsDto
var response = new ContentsDto
{
Total = result.Contents.Total,
Items = result.Contents.Take(200).Select(item =>
@ -161,7 +161,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -179,7 +179,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -197,7 +197,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -215,7 +215,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -230,7 +230,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -245,7 +245,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -260,7 +260,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
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);
@ -275,7 +275,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
{
await contentQuery.FindSchemaAsync(App, name);
var command = new DeleteContent { ContentId = id, User = User };
var command = new DeleteContent { ContentId = id };
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.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger;
using Squidex.Shared.Identity;
@ -32,7 +33,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
private readonly JsonSchema4 dataSchema;
private readonly string schemaPath;
private readonly string schemaName;
private readonly string schemaKey;
private readonly string schemaType;
private readonly string appPath;
static SchemaSwaggerGenerator()
@ -68,12 +69,12 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
appPath = path;
schemaPath = schema.Name;
schemaName = schema.Properties.Label.WithFallback(schema.Name);
schemaKey = schema.Name.ToPascalCase();
schemaName = schema.DisplayName();
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()
@ -108,13 +109,13 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
return AddOperation(SwaggerOperationMethod.Get, null, $"{appPath}/{schemaPath}", operation =>
{
operation.OperationId = $"Query{schemaKey}Contents";
operation.OperationId = $"Query{schemaType}Contents";
operation.Summary = $"Queries {schemaName} contents.";
operation.Security = ReaderSecurity;
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("$filter", JsonObjectType.String, "Optional OData filter.");
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 =>
{
operation.OperationId = $"Get{schemaKey}Content";
operation.OperationId = $"Get{schemaType}Content";
operation.Summary = $"Get a {schemaName} content.";
operation.Security = ReaderSecurity;
@ -140,7 +141,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.Security = EditorSecurity;
@ -155,7 +156,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.Security = EditorSecurity;
@ -169,8 +170,8 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appPath}/{schemaPath}/{{id}}", operation =>
{
operation.OperationId = $"Path{schemaKey}Content";
operation.Summary = $"Patchs a {schemaName} content.";
operation.OperationId = $"Path{schemaType}Content";
operation.Summary = $"Patch a {schemaName} content.";
operation.Security = EditorSecurity;
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 =>
{
operation.OperationId = $"Publish{schemaKey}Content";
operation.OperationId = $"Publish{schemaType}Content";
operation.Summary = $"Publish a {schemaName} content.";
operation.Security = EditorSecurity;
@ -195,7 +196,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.Security = EditorSecurity;
@ -207,7 +208,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.Security = EditorSecurity;
@ -219,7 +220,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.Security = EditorSecurity;
@ -231,7 +232,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Generator
{
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.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
{
public sealed class AssetsDto
public sealed class ContentsDto
{
/// <summary>
/// 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)
{
if (context.Command is SquidexCommand squidexCommand && squidexCommand.Actor == null)
if (context.Command is SquidexCommand squidexCommand)
{
var actorToken =
FindActorFromSubject() ??
FindActorFromClient();
if (squidexCommand.Actor == null)
{
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();

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.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
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 Xunit;
#pragma warning disable SA1311 // Static readonly fields must begin with upper-case letter
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]
[InlineData(null)]
[InlineData("")]
@ -103,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -111,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
const string query = @"
query {
queryAssets(search: ""my-query"", top: 30, skip: 5) {
queryAssets(search: ""my-query"", take: 30, skip: 5) {
id
version
created
@ -169,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -177,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
const string query = @"
query {
queryAssetsWithTotal(search: ""my-query"", top: 30, skip: 5) {
queryAssetsWithTotal(search: ""my-query"", take: 30, skip: 5) {
total
items {
id
@ -242,7 +172,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -304,7 +234,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -417,7 +347,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -537,7 +467,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -646,7 +576,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -706,7 +636,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -766,7 +696,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
};
AssertJson(expected, new { data = result.Data });
AssertResult(expected, result);
}
[Fact]
@ -803,78 +733,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
data = (object)null
};
AssertJson(expected, new { data = result.Data });
}
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);
AssertResult(expected, result, false);
}
}
}

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