mirror of https://github.com/Squidex/squidex.git
31 changed files with 1285 additions and 399 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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>(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()) |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue