Browse Source

Merge pull request #381 from Squidex/next

Next
pull/387/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
f0de880f8f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      .drone.yml
  2. 4
      Dockerfile
  3. 4
      Dockerfile.build
  4. 1
      Squidex.ruleset
  5. 2
      src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  7. 6
      src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  8. 16
      src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  9. 4
      src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  10. 2
      src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  11. 42
      src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs
  12. 37
      src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs
  13. 54
      src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  14. 5
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
  15. 16
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs
  16. 36
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs
  17. 32
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
  18. 18
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
  19. 125
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  20. 31
      src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  21. 20
      src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
  22. 84
      src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  23. 2
      src/Squidex.Domain.Apps.Core.Model/Named.cs
  24. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
  25. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  26. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  27. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  28. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
  29. 10
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  30. 1
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  31. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs
  32. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  33. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs
  34. 11
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  35. 3
      src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs
  36. 4
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
  37. 6
      src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
  38. 2
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs
  39. 26
      src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs
  40. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  41. 1
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  42. 13
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  43. 39
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs
  44. 6
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
  45. 32
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  46. 103
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  47. 9
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  48. 23
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs
  49. 10
      src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs
  50. 19
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs
  51. 109
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs
  52. 3
      src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
  53. 14
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
  54. 4
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs
  55. 4
      src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs
  56. 2
      src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
  57. 19
      src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
  58. 87
      src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
  59. 20
      src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
  60. 82
      src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs
  61. 9
      src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  62. 26
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  63. 99
      src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
  64. 2
      src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  65. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  66. 9
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  67. 20
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
  68. 19
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs
  69. 6
      src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  70. 16
      src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
  71. 2
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  72. 6
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  73. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs
  74. 8
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs
  75. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs
  76. 2
      src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs
  77. 7
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  78. 46
      src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
  79. 111
      src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
  80. 29
      src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
  81. 88
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  82. 223
      src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
  83. 139
      src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs
  84. 96
      src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  85. 57
      src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs
  86. 153
      src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  87. 6
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  88. 12
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  89. 4
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs
  90. 5
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
  91. 10
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  92. 18
      src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs
  93. 57
      src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  94. 20
      src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs
  95. 9
      src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs
  96. 31
      src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  97. 20
      src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs
  98. 19
      src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs
  99. 12
      src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs
  100. 4
      src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs

6
.drone.yml

@ -20,7 +20,7 @@ steps:
image: docker
commands:
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- docker build -t squidex/squidex:dev -t squidex/squidex:dev-$${DRONE_BUILD_NUMBER} .
- docker build -t squidex/squidex:dev -t squidex/squidex:dev-$${DRONE_BUILD_NUMBER} --build-arg SQUIDEX__VERSION=dev-$${DRONE_BUILD_NUMBER} .
- docker push squidex/squidex:dev
- docker push squidex/squidex:dev-$${DRONE_BUILD_NUMBER}
volumes:
@ -43,7 +43,7 @@ steps:
image: docker
commands:
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- docker build -t squidex/squidex:latest -t squidex/squidex:$${DRONE_TAG} .
- docker build -t squidex/squidex:latest -t squidex/squidex:$${DRONE_TAG} --build-arg SQUIDEX__VERSION=$${DRONE_TAG} .
- docker push squidex/squidex:latest
- docker push squidex/squidex:$${DRONE_TAG}
volumes:
@ -63,7 +63,7 @@ steps:
- name: build_binaries
image: docker
commands:
- docker build . -t squidex-build-image -f Dockerfile.build
- docker build -t squidex-build-image -f Dockerfile.build --build-arg SQUIDEX__VERSION=$${DRONE_TAG} .
- docker create --name squidex-build-container squidex-build-image
- docker cp squidex-build-container:/out /build
volumes:

4
Dockerfile

@ -3,6 +3,8 @@
#
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
ARG SQUIDEX__VERSION=1.0.0
WORKDIR /src
# Copy Node project files.
@ -35,7 +37,7 @@ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.
&& dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64 -p:version=$SQUIDEX__VERSION
#
# Stage 2, Build runtime

4
Dockerfile.build

@ -1,5 +1,7 @@
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
ARG SQUIDEX__VERSION=1.0.0
WORKDIR /src
# Copy Node project files.
@ -32,4 +34,4 @@ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.
&& dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release -p:version=$SQUIDEX__VERSION

1
Squidex.ruleset

@ -63,6 +63,7 @@
<Rule Id="SA1601" Action="None" />
<Rule Id="SA1413" Action="None" />
<Rule Id="SA0001" Action="None" />
<Rule Id="SA1602" Action="None" />
</Rules>
<Rules AnalyzerId="RefactoringEssentials" RuleNamespace="RefactoringEssentials">
<Rule Id="RECS0061" Action="Error" />

2
src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Apps
{
Guard.NotNullOrEmpty(secret, nameof(secret));
Guard.NotNullOrEmpty(role, nameof(role));
Role = role;
Secret = secret;

2
src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.Apps
: base(name)
{
Guard.NotNullOrEmpty(pattern, nameof(pattern));
Pattern = pattern;
Message = message;

6
src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Core.Apps.Json
{

16
src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -5,10 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using P = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps
@ -20,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Apps
public const string Owner = "Owner";
public const string Reader = "Reader";
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>
private static readonly HashSet<string> DefaultRolesSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
Editor,
Developer,
@ -54,6 +55,11 @@ namespace Squidex.Domain.Apps.Core.Apps
return role != null && DefaultRolesSet.Contains(role);
}
public static bool IsRole(string name, string expected)
{
return name != null && string.Equals(name, expected, StringComparison.OrdinalIgnoreCase);
}
public static Role CreateOwner(string app)
{
return new Role(Owner,
@ -65,7 +71,8 @@ namespace Squidex.Domain.Apps.Core.Apps
return new Role(Editor,
P.ForApp(P.AppAssets, app),
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app));
P.ForApp(P.AppContents, app),
P.ForApp(P.AppWorkflowsRead, app));
}
public static Role CreateReader(string app)
@ -84,6 +91,7 @@ namespace Squidex.Domain.Apps.Core.Apps
P.ForApp(P.AppCommon, app),
P.ForApp(P.AppContents, app),
P.ForApp(P.AppPatterns, app),
P.ForApp(P.AppWorkflows, app),
P.ForApp(P.AppRules, app),
P.ForApp(P.AppSchemas, app));
}

4
src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{

2
src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure;
using System;
namespace Squidex.Domain.Apps.Core.Comments
{

42
src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class StatusConverter : JsonConverter, ISupportedTypes
{
public IEnumerable<Type> SupportedTypes
{
get { yield return typeof(Status); }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.String)
{
throw new JsonException($"Expected String, but got {reader.TokenType}.");
}
return new Status(reader.Value.ToString());
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Status);
}
}
}

37
src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs

@ -0,0 +1,37 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Squidex.Infrastructure.Json.Newtonsoft;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class WorkflowConverter : JsonClassConverter<Workflows>
{
protected override void WriteValue(JsonWriter writer, Workflows value, JsonSerializer serializer)
{
var json = new Dictionary<Guid, Workflow>(value.Count);
foreach (var workflow in value)
{
json.Add(workflow.Key, workflow.Value);
}
serializer.Serialize(writer, json);
}
protected override Workflows ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var json = serializer.Deserialize<Dictionary<Guid, Workflow>>(reader);
return new Workflows(json.ToArray());
}
}
}

54
src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -5,12 +5,58 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel;
namespace Squidex.Domain.Apps.Core.Contents
{
public enum Status
[TypeConverter(typeof(StatusConverter))]
public struct Status : IEquatable<Status>
{
Draft,
Archived,
Published
public static readonly Status Archived = new Status("Archived");
public static readonly Status Draft = new Status("Draft");
public static readonly Status Published = new Status("Published");
private readonly string name;
public string Name
{
get { return name ?? "Unknown"; }
}
public Status(string name)
{
this.name = name;
}
public override bool Equals(object obj)
{
return obj is Status status && Equals(status);
}
public bool Equals(Status other)
{
return string.Equals(name, other.name);
}
public override int GetHashCode()
{
return name?.GetHashCode() ?? 0;
}
public override string ToString()
{
return Name;
}
public static bool operator ==(Status lhs, Status rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(Status lhs, Status rhs)
{
return !lhs.Equals(rhs);
}
}
}

5
src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs

@ -1,7 +1,7 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
@ -9,9 +9,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
public enum StatusChange
{
Archived,
Change,
Published,
Restored,
Unpublished
}
}

16
src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusColors
{
public const string Archived = "#eb3142";
public const string Draft = "#8091a5";
public const string Published = "#4bb958";
}
}

36
src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel;
using System.Globalization;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class StatusConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new Status(value?.ToString());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
return value.ToString();
}
}
}

32
src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Domain.Apps.Core.Contents
{
public static class StatusFlow
{
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]>
{
[Status.Draft] = new[] { Status.Published, Status.Archived },
[Status.Archived] = new[] { Status.Draft },
[Status.Published] = new[] { Status.Draft, Status.Archived }
};
public static bool Exists(Status status)
{
return Flow.ContainsKey(status);
}
public static bool CanChange(Status status, Status toStatus)
{
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus);
}
}
}

18
src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs → src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs

@ -5,19 +5,19 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class ContentDataChangedResult : EntitySavedResult
public sealed class StatusInfo
{
public NamedContentData Data { get; }
public Status Status { get; }
public string Color { get; }
public ContentDataChangedResult(NamedContentData data, long version)
: base(version)
public StatusInfo(Status status, string color)
{
Data = data;
Status = status;
Color = color;
}
}
}

125
src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -0,0 +1,125 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflow : Named
{
private const string DefaultName = "Unnamed";
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly IReadOnlyList<Guid> EmptySchemaIds = new List<Guid>();
public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, EmptySteps);
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds;
public Status Initial { get; }
public Workflow(
Status initial,
IReadOnlyDictionary<Status, WorkflowStep> steps,
IReadOnlyList<Guid> schemaIds = null,
string name = null)
: base(name ?? DefaultName)
{
Initial = initial;
if (steps != null)
{
Steps = steps;
}
if (schemaIds != null)
{
SchemaIds = schemaIds;
}
}
public static Workflow CreateDefault(string name = null)
{
return new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Archived] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Archived, true),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition()
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
}, null, name);
}
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status)
{
if (TryGetStep(status, out var step))
{
foreach (var transition in step.Transitions)
{
yield return (transition.Key, Steps[transition.Key], transition.Value);
}
}
else if (TryGetStep(Initial, out var initial))
{
yield return (Initial, initial, WorkflowTransition.Default);
}
}
public bool TryGetTransition(Status from, Status to, out WorkflowTransition transition)
{
transition = null;
if (TryGetStep(from, out var step))
{
if (step.Transitions.TryGetValue(to, out transition))
{
return true;
}
}
else if (to == Initial)
{
transition = WorkflowTransition.Default;
return true;
}
return false;
}
public bool TryGetStep(Status status, out WorkflowStep step)
{
return Steps.TryGetValue(status, out step);
}
public (Status Key, WorkflowStep) GetInitialStep()
{
return (Initial, Steps[Initial]);
}
}
}

31
src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class WorkflowStep
{
private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>();
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string Color { get; }
public bool NoUpdate { get; }
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition> transitions = null, string color = null, bool noUpdate = false)
{
Transitions = transitions ?? EmptyTransitions;
Color = color;
NoUpdate = noUpdate;
}
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs → src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs

@ -5,21 +5,21 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
namespace Squidex.Domain.Apps.Core.Contents
{
public class AssetSavedResult : EntitySavedResult
public sealed class WorkflowTransition
{
public long FileVersion { get; }
public static readonly WorkflowTransition Default = new WorkflowTransition();
public string Expression { get; }
public string FileHash { get; }
public string Role { get; }
public AssetSavedResult(long version, long fileVersion, string fileHash)
: base(version)
public WorkflowTransition(string expression = null, string role = null)
{
FileVersion = fileVersion;
FileHash = fileHash;
Expression = expression;
Role = role;
}
}
}

84
src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -0,0 +1,84 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflows : ArrayDictionary<Guid, Workflow>
{
public static readonly Workflows Empty = new Workflows();
private Workflows()
{
}
public Workflows(KeyValuePair<Guid, Workflow>[] items)
: base(items)
{
}
[Pure]
public Workflows Remove(Guid id)
{
return new Workflows(Without(id));
}
[Pure]
public Workflows Add(Guid workflowId, string name)
{
Guard.NotNullOrEmpty(name, nameof(name));
return new Workflows(With(workflowId, Workflow.CreateDefault(name)));
}
[Pure]
public Workflows Set(Workflow workflow)
{
Guard.NotNull(workflow, nameof(workflow));
return new Workflows(With(Guid.Empty, workflow));
}
[Pure]
public Workflows Set(Guid id, Workflow workflow)
{
Guard.NotNull(workflow, nameof(workflow));
return new Workflows(With(id, workflow));
}
[Pure]
public Workflows Update(Guid id, Workflow workflow)
{
Guard.NotNull(workflow, nameof(workflow));
if (id == Guid.Empty)
{
return Set(workflow);
}
if (!ContainsKey(id))
{
return this;
}
return new Workflows(With(id, workflow));
}
public Workflow GetFirst()
{
return Values.FirstOrDefault() ?? Workflow.Default;
}
}
}

2
src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs → src/Squidex.Domain.Apps.Core.Model/Named.cs

@ -7,7 +7,7 @@
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
namespace Squidex.Domain.Apps.Core
{
public abstract class Named
{

2
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs

@ -5,8 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System.Collections.ObjectModel;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.Triggers
{

2
src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs

@ -5,10 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{

2
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs

@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class FieldCollection<T> : Cloneable<FieldCollection<T>> where T : IField
{
public static readonly FieldCollection<T> Empty = new FieldCollection<T>();
private static readonly Dictionary<long, T> EmptyById = new Dictionary<long, T>();
private static readonly Dictionary<string, T> EmptyByString = new Dictionary<string, T>();

2
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using NamedIdStatic = Squidex.Infrastructure.NamedId;
namespace Squidex.Domain.Apps.Core.Schemas

4
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs

@ -5,9 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using System;
using P = Squidex.Domain.Apps.Core.Partitioning;
namespace Squidex.Domain.Apps.Core.Schemas.Json
@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
if (Properties is ArrayFieldProperties arrayProperties)
{
var nested = Children?.ToArray(n => n.ToNestedField()) ?? Array.Empty<NestedField>();
var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty<NestedField>();
return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this);
}

10
src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
SimpleMapper.Map(schema, this);
Fields =
schema.Fields.ToArray(x =>
schema.Fields.Select(x =>
new JsonFieldModel
{
Id = x.Id,
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
IsDisabled = x.IsDisabled,
Partitioning = x.Partitioning.Key,
Properties = x.RawProperties
});
}).ToArray();
PreviewUrls = schema.PreviewUrls.ToDictionary(x => x.Key, x => x.Value);
}
@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
{
if (field is ArrayField arrayField)
{
return arrayField.Fields.ToArray(x =>
return arrayField.Fields.Select(x =>
new JsonNestedFieldModel
{
Id = x.Id,
@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
Properties = x.RawProperties
});
}).ToArray();
}
return null;
@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public Schema ToSchema()
{
var fields = Fields.ToArray(f => f.ToField()) ?? Array.Empty<RootField>();
var fields = Fields.Map(f => f.ToField()) ?? Array.Empty<RootField>();
var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton);

1
src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class JsonFieldProperties : FieldProperties

2
src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs

@ -5,8 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{

2
src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj

@ -14,6 +14,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Freezable.Fody" Version="1.9.3" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />

5
src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs

@ -9,12 +9,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents
{
public enum EnrichedContentEventType
{
Archived,
Created,
Deleted,
Published,
Restored,
StatusChanged,
Updated,
Unpublished,
Updated
}
}

11
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -47,6 +47,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("APP_ID", AppId);
AddPattern("APP_NAME", AppName);
AddPattern("CONTENT_ACTION", ContentAction);
AddPattern("CONTENT_STATUS", ContentStatus);
AddPattern("CONTENT_URL", ContentUrl);
AddPattern("SCHEMA_ID", SchemaId);
AddPattern("SCHEMA_NAME", SchemaName);
@ -212,6 +213,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return Fallback;
}
private static string ContentStatus(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)
{
return contentEvent.Status.ToString();
}
return Fallback;
}
private string ContentUrl(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)

3
src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs

@ -46,6 +46,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
case Instant instant:
result = JsValue.FromObject(engine, instant.ToDateTimeUtc());
return true;
case Status status:
result = status.ToString();
return true;
case NamedContentData content:
result = new ContentDataObject(engine, content);
return true;

4
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs

@ -48,9 +48,9 @@ namespace Squidex.Domain.Apps.Core.Scripting
private static ObjectWrapper CreateUser(Engine engine, string id, bool isClient, string email, string name, IEnumerable<Claim> allClaims)
{
var claims =
allClaims.GroupBy(x => x.Type)
allClaims.GroupBy(x => x.Type.Split(ClaimSeparators).Last())
.ToDictionary(
x => x.Key.Split(ClaimSeparators).Last(),
x => x.Key,
x => x.Select(y => y.Value).ToArray());
return new ObjectWrapper(engine, new { id, isClient, email, name, claims });

6
src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs

@ -19,11 +19,11 @@ namespace Squidex.Domain.Apps.Core.Tags
Task<Dictionary<string, string>> DenormalizeTagsAsync(Guid appId, string group, HashSet<string> ids);
Task<Dictionary<string, int>> GetTagsAsync(Guid appId, string group);
Task<TagsSet> GetTagsAsync(Guid appId, string group);
Task<TagSet> GetExportableTagsAsync(Guid appId, string group);
Task<TagsExport> GetExportableTagsAsync(Guid appId, string group);
Task RebuildTagsAsync(Guid appId, string group, TagSet tags);
Task RebuildTagsAsync(Guid appId, string group, TagsExport tags);
Task ClearAsync(Guid appId, string group);
}

2
src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs → src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs

@ -9,7 +9,7 @@ using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Tags
{
public sealed class TagSet : Dictionary<string, Tag>
public sealed class TagsExport : Dictionary<string, Tag>
{
}
}

26
src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Tags
{
public sealed class TagsSet : Dictionary<string, int>
{
public long Version { get; set; }
public TagsSet()
{
}
public TagsSet(IDictionary<string, int> tags, long version)
: base(tags)
{
Version = version;
}
}
}

2
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
public async Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{

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

@ -51,7 +51,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired]
[BsonElement("ss")]
[BsonRepresentation(BsonType.String)]
public Status Status { get; set; }
[BsonIgnoreIfNull]

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

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly string typeContentDeleted;
private readonly MongoContentCollection contents;
static MongoContentRepository()
{
StatusSerializer.Register();
}
public MongoContentRepository(IMongoDatabase database, IAppProvider appProvider, IJsonSerializer serializer, ITextIndexer indexer, TypeNameRegistry typeNameRegistry)
{
Guard.NotNull(appProvider, nameof(appProvider));
@ -64,7 +69,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
Guard.NotNull(query, nameof(query));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByQuery"))
@ -73,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (fullTextIds?.Count == 0)
{
return ResultList.Create<IContentEntity>(0);
return ResultList.CreateFrom<IContentEntity>(0);
}
return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft);
@ -83,9 +87,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
Guard.NotNull(schema, nameof(schema));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIds"))
{
@ -96,7 +99,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(IAppEntity app, Status[] status, HashSet<Guid> ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
using (Profiler.TraceMethod<MongoContentRepository>("QueryAsyncByIdsWithoutSchema"))
@ -109,7 +111,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
Guard.NotNull(app, nameof(app));
Guard.NotNull(schema, nameof(schema));
Guard.NotNull(status, nameof(status));
using (Profiler.TraceMethod<MongoContentRepository>())
{

39
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
public sealed class StatusSerializer : SerializerBase<Status>
{
private static volatile int isRegistered;
public static void Register()
{
if (Interlocked.Increment(ref isRegistered) == 1)
{
BsonSerializer.RegisterSerializer(new StatusSerializer());
}
}
public override Status Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var value = context.Reader.ReadString();
return new Status(value);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status value)
{
context.Writer.WriteString(value.Name);
}
}
}

6
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs

@ -162,7 +162,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
}
filters.Add(Filter.Ne(x => x.IsDeleted, true));
filters.Add(Filter.In(x => x.Status, status));
if (status != null)
{
filters.Add(Filter.In(x => x.Status, status));
}
if (ids != null && ids.Count > 0)
{

32
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -65,6 +65,17 @@ namespace Squidex.Domain.Apps.Entities
});
}
public Task<IAppEntity> GetAppAsync(Guid appId)
{
return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () =>
{
using (Profiler.TraceMethod<AppProvider>())
{
return await GetAppByIdAsync(appId);
}
});
}
public Task<IAppEntity> GetAppAsync(string appName)
{
return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
@ -78,14 +89,7 @@ namespace Squidex.Domain.Apps.Entities
return null;
}
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (!IsExisting(app))
{
return null;
}
return app.Value;
return await GetAppByIdAsync(appId);
}
});
}
@ -184,6 +188,18 @@ namespace Squidex.Domain.Apps.Entities
});
}
private async Task<IAppEntity> GetAppByIdAsync(Guid appId)
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (!IsExisting(app))
{
return null;
}
return app.Value;
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())

103
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -60,11 +60,13 @@ namespace Squidex.Domain.Apps.Entities.Apps
switch (command)
{
case CreateApp createApp:
return CreateAsync(createApp, c =>
return CreateReturn(createApp, c =>
{
GuardApp.CanCreate(c);
Create(c);
return Snapshot;
});
case AssignContributor assignContributor:
@ -74,111 +76,167 @@ namespace Squidex.Domain.Apps.Entities.Apps
AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId));
return EntityCreatedResult.Create(c.ContributorId, Version);
return Snapshot;
});
case RemoveContributor removeContributor:
return UpdateAsync(removeContributor, c =>
return UpdateReturn(removeContributor, c =>
{
GuardAppContributors.CanRemove(Snapshot.Contributors, c);
RemoveContributor(c);
return Snapshot;
});
case AttachClient attachClient:
return UpdateAsync(attachClient, c =>
return UpdateReturn(attachClient, c =>
{
GuardAppClients.CanAttach(Snapshot.Clients, c);
AttachClient(c);
return Snapshot;
});
case UpdateClient updateClient:
return UpdateAsync(updateClient, c =>
return UpdateReturn(updateClient, c =>
{
GuardAppClients.CanUpdate(Snapshot.Clients, c, Snapshot.Roles);
UpdateClient(c);
return Snapshot;
});
case RevokeClient revokeClient:
return UpdateAsync(revokeClient, c =>
return UpdateReturn(revokeClient, c =>
{
GuardAppClients.CanRevoke(Snapshot.Clients, c);
RevokeClient(c);
return Snapshot;
});
case AddWorkflow addWorkflow:
return UpdateReturn(addWorkflow, c =>
{
GuardAppWorkflows.CanAdd(c);
AddWorkflow(c);
return Snapshot;
});
case UpdateWorkflow updateWorkflow:
return UpdateReturn(updateWorkflow, c =>
{
GuardAppWorkflows.CanUpdate(Snapshot.Workflows, c);
UpdateWorkflow(c);
return Snapshot;
});
case DeleteWorkflow deleteWorkflow:
return UpdateReturn(deleteWorkflow, c =>
{
GuardAppWorkflows.CanDelete(Snapshot.Workflows, c);
DeleteWorkflow(c);
return Snapshot;
});
case AddLanguage addLanguage:
return UpdateAsync(addLanguage, c =>
return UpdateReturn(addLanguage, c =>
{
GuardAppLanguages.CanAdd(Snapshot.LanguagesConfig, c);
AddLanguage(c);
return Snapshot;
});
case RemoveLanguage removeLanguage:
return UpdateAsync(removeLanguage, c =>
return UpdateReturn(removeLanguage, c =>
{
GuardAppLanguages.CanRemove(Snapshot.LanguagesConfig, c);
RemoveLanguage(c);
return Snapshot;
});
case UpdateLanguage updateLanguage:
return UpdateAsync(updateLanguage, c =>
return UpdateReturn(updateLanguage, c =>
{
GuardAppLanguages.CanUpdate(Snapshot.LanguagesConfig, c);
UpdateLanguage(c);
return Snapshot;
});
case AddRole addRole:
return UpdateAsync(addRole, c =>
return UpdateReturn(addRole, c =>
{
GuardAppRoles.CanAdd(Snapshot.Roles, c);
AddRole(c);
return Snapshot;
});
case DeleteRole deleteRole:
return UpdateAsync(deleteRole, c =>
return UpdateReturn(deleteRole, c =>
{
GuardAppRoles.CanDelete(Snapshot.Roles, c, Snapshot.Contributors, Snapshot.Clients);
DeleteRole(c);
return Snapshot;
});
case UpdateRole updateRole:
return UpdateAsync(updateRole, c =>
return UpdateReturn(updateRole, c =>
{
GuardAppRoles.CanUpdate(Snapshot.Roles, c);
UpdateRole(c);
return Snapshot;
});
case AddPattern addPattern:
return UpdateAsync(addPattern, c =>
return UpdateReturn(addPattern, c =>
{
GuardAppPatterns.CanAdd(Snapshot.Patterns, c);
AddPattern(c);
return Snapshot;
});
case DeletePattern deletePattern:
return UpdateAsync(deletePattern, c =>
return UpdateReturn(deletePattern, c =>
{
GuardAppPatterns.CanDelete(Snapshot.Patterns, c);
DeletePattern(c);
return Snapshot;
});
case UpdatePattern updatePattern:
return UpdateAsync(updatePattern, c =>
return UpdateReturn(updatePattern, c =>
{
GuardAppPatterns.CanUpdate(Snapshot.Patterns, c);
UpdatePattern(c);
return Snapshot;
});
case ChangePlan changePlan:
@ -291,6 +349,21 @@ namespace Squidex.Domain.Apps.Entities.Apps
RaiseEvent(SimpleMapper.Map(command, new AppClientRevoked()));
}
public void AddWorkflow(AddWorkflow command)
{
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowAdded()));
}
public void UpdateWorkflow(UpdateWorkflow command)
{
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowUpdated()));
}
public void DeleteWorkflow(DeleteWorkflow command)
{
RaiseEvent(SimpleMapper.Map(command, new AppWorkflowDeleted()));
}
public void AddLanguage(AddLanguage command)
{
RaiseEvent(SimpleMapper.Map(command, new AppLanguageAdded()));

9
src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs

@ -19,15 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps
public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry)
: base(typeNameRegistry)
{
AddEventMessage("AppContributorAssignedEvent",
"assigned {user:[Contributor]} as {[Role]}");
AddEventMessage("AppClientUpdatedEvent",
"updated client {[Id]}");
AddEventMessage("AppPlanChanged",
"changed plan to {[Plan]}");
AddEventMessage<AppContributorAssigned>(
"assigned {user:[Contributor]} as {[Role]}");

23
src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class AddWorkflow : AppCommand
{
public Guid WorkflowId { get; set; }
public string Name { get; set; }
public AddWorkflow()
{
WorkflowId = Guid.NewGuid();
}
}
}

10
src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs → src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs

@ -5,12 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Contents
using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public enum StatusForFrontend
public sealed class DeleteWorkflow : AppCommand
{
PublishedDraft,
PublishedOnly,
Archived
public Guid WorkflowId { get; set; }
}
}

19
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public sealed class UpdateWorkflow : AppCommand
{
public Guid WorkflowId { get; set; }
public Workflow Workflow { get; set; }
}
}

109
src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs

@ -0,0 +1,109 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
public static class GuardAppWorkflows
{
public static void CanAdd(AddWorkflow command)
{
Guard.NotNull(command, nameof(command));
Validate.It(() => "Cannot add workflow.", e =>
{
if (string.IsNullOrWhiteSpace(command.Name))
{
e(Not.Defined("Name"), nameof(command.Name));
}
});
}
public static void CanUpdate(Workflows workflows, UpdateWorkflow command)
{
Guard.NotNull(command, nameof(command));
GetWorkflowOrThrow(workflows, command.WorkflowId);
Validate.It(() => "Cannot update workflow.", e =>
{
if (command.Workflow == null)
{
e(Not.Defined("Workflow"), nameof(command.Workflow));
return;
}
var workflow = command.Workflow;
if (!workflow.Steps.ContainsKey(workflow.Initial))
{
e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}");
}
if (workflow.Initial == Status.Published)
{
e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}");
}
var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}";
if (!workflow.Steps.ContainsKey(Status.Published))
{
e("Workflow must have a published step.", stepsPrefix);
}
foreach (var step in workflow.Steps)
{
var stepPrefix = $"{stepsPrefix}.{step.Key}";
if (step.Value == null)
{
e(Not.Defined("Step"), stepPrefix);
}
else
{
foreach (var transition in step.Value.Transitions)
{
var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}";
if (!workflow.Steps.ContainsKey(transition.Key))
{
e("Transition has an invalid target.", transitionPrefix);
}
if (transition.Value == null)
{
e(Not.Defined("Transition"), transitionPrefix);
}
}
}
}
});
}
public static void CanDelete(Workflows workflows, DeleteWorkflow command)
{
Guard.NotNull(command, nameof(command));
GetWorkflowOrThrow(workflows, command.WorkflowId);
}
private static Workflow GetWorkflowOrThrow(Workflows workflows, Guid id)
{
if (!workflows.TryGetValue(id, out var workflow))
{
throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity));
}
return workflow;
}
}
}

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

@ -6,6 +6,7 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Apps
{
@ -29,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
LanguagesConfig LanguagesConfig { get; }
Workflows Workflows { get; }
bool IsArchived { get; }
}
}

14
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId);
break;
case AssignContributor assignContributor:
await Index(GetUserId(context)).AddAppAsync(assignContributor.AppId);
await Index(GetUserId(assignContributor)).AddAppAsync(assignContributor.AppId);
break;
case RemoveContributor removeContributor:
await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId);
@ -57,19 +57,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
await next();
}
private static string GetUserId(RemoveContributor removeContributor)
private static string GetUserId(CreateApp createApp)
{
return removeContributor.ContributorId;
return createApp.Actor.Identifier;
}
private static string GetUserId(CreateApp createApp)
private static string GetUserId(AssignContributor assignContributor)
{
return createApp.Actor.Identifier;
return assignContributor.ContributorId;
}
private static string GetUserId(CommandContext context)
private static string GetUserId(RemoveContributor removeContributor)
{
return context.Result<EntityCreatedResult<string>>().IdOrValue;
return removeContributor.ContributorId;
}
private IAppsByUserIndex Index(string id)

4
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs

@ -35,9 +35,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation
await next();
if (assignContributor.IsCreated && context.PlainResult is EntityCreatedResult<string> id)
if (assignContributor.IsCreated && context.PlainResult is IAppEntity app)
{
context.Complete(new InvitedResult { Id = id });
context.Complete(new InvitedResult { App = app });
}
return;

4
src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Invitation
{
public sealed class InvitedResult
{
public EntityCreatedResult<string> Id { get; set; }
public IAppEntity App { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs

@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
return id;
}
}));
}).Where(x => x != "common"));
}
}
}

19
src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs

@ -7,6 +7,7 @@
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure.Dispatching;
@ -42,6 +43,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
[DataMember]
public LanguagesConfig LanguagesConfig { get; set; } = LanguagesConfig.English;
[DataMember]
public Workflows Workflows { get; set; } = Workflows.Empty;
[DataMember]
public bool IsArchived { get; set; }
@ -92,6 +96,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.State
Clients = Clients.Revoke(@event.Id);
}
protected void On(AppWorkflowAdded @event)
{
Workflows = Workflows.Add(@event.WorkflowId, @event.Name);
}
protected void On(AppWorkflowUpdated @event)
{
Workflows = Workflows.Update(@event.WorkflowId, @event.Workflow);
}
protected void On(AppWorkflowDeleted @event)
{
Workflows = Workflows.Remove(@event.WorkflowId);
}
protected void On(AppPatternAdded @event)
{
Patterns = Patterns.Add(@event.PatternId, @event.Name, @event.Pattern, @event.Message);

87
src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs

@ -21,27 +21,30 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware<AssetCommand, IAssetGrain>
{
private readonly IAssetStore assetStore;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable<ITagGenerator<CreateAsset>> tagGenerators;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
IAssetEnricher assetEnricher,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable<ITagGenerator<CreateAsset>> tagGenerators)
: base(grainFactory)
{
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator));
Guard.NotNull(tagGenerators, nameof(tagGenerators));
this.assetStore = assetStore;
this.assetEnricher = assetEnricher;
this.assetQuery = assetQuery;
this.assetThumbnailGenerator = assetThumbnailGenerator;
this.tagGenerators = tagGenerators;
}
@ -56,53 +59,37 @@ namespace Squidex.Domain.Apps.Entities.Assets
createAsset.Tags = new HashSet<string>();
}
createAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(createAsset.File.OpenRead());
createAsset.FileHash = await UploadAsync(context, createAsset.File);
await EnrichWithImageInfosAsync(createAsset);
await EnrichWithHashAndUploadAsync(createAsset, context);
try
{
var existings = await assetQuery.QueryByHashAsync(createAsset.AppId.Id, createAsset.FileHash);
AssetCreatedResult result = null;
foreach (var existing in existings)
{
if (IsDuplicate(createAsset, existing))
{
result = new AssetCreatedResult(
existing.Id,
existing.Tags,
existing.Version,
existing.FileVersion,
existing.FileHash,
true);
}
var result = new AssetCreatedResult(existing, true);
break;
context.Complete(result);
await next();
return;
}
}
if (result == null)
foreach (var tagGenerator in tagGenerators)
{
foreach (var tagGenerator in tagGenerators)
{
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
tagGenerator.GenerateTags(createAsset, createAsset.Tags);
}
var commandResult = (AssetSavedResult)await ExecuteCommandAsync(createAsset);
await HandleCoreAsync(context, next);
result = new AssetCreatedResult(
createAsset.AssetId,
createAsset.Tags,
commandResult.Version,
commandResult.FileVersion,
commandResult.FileHash,
false);
var asset = context.PlainResult as IEnrichedAssetEntity;
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), result.FileVersion, null);
}
context.Complete(new AssetCreatedResult(asset, false));
context.Complete(result);
await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null);
}
finally
{
@ -114,16 +101,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
case UpdateAsset updateAsset:
{
updateAsset.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(updateAsset.File.OpenRead());
await EnrichWithImageInfosAsync(updateAsset);
await EnrichWithHashAndUploadAsync(updateAsset, context);
updateAsset.FileHash = await UploadAsync(context, updateAsset.File);
try
{
var result = (AssetSavedResult)await ExecuteCommandAsync(updateAsset);
await HandleCoreAsync(context, next);
context.Complete(result);
var asset = context.PlainResult as IAssetEntity;
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.FileVersion, null);
await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), asset.FileVersion, null);
}
finally
{
@ -134,28 +121,42 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
default:
await base.HandleAsync(context, next);
await HandleCoreAsync(context, next);
break;
}
}
private async Task HandleCoreAsync(CommandContext context, Func<Task> next)
{
await base.HandleAsync(context, next);
if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity))
{
var enriched = await assetEnricher.EnrichAsync(asset);
context.Complete(enriched);
}
}
private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset)
{
return asset != null && asset.FileName == createAsset.File.FileName && asset.FileSize == createAsset.File.FileSize;
}
private async Task<string> UploadAsync(CommandContext context, AssetFile file)
private async Task EnrichWithImageInfosAsync(UploadAssetCommand command)
{
string hash;
command.ImageInfo = await assetThumbnailGenerator.GetImageInfoAsync(command.File.OpenRead());
}
using (var hashStream = new HasherStream(file.OpenRead(), HashAlgorithmName.SHA256))
private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, CommandContext context)
{
using (var hashStream = new HasherStream(command.File.OpenRead(), HashAlgorithmName.SHA256))
{
await assetStore.UploadAsync(context.ContextId.ToString(), hashStream);
hash = $"{hashStream.GetHashStringAndReset()}{file.FileName}{file.FileSize}".Sha256Base64();
command.FileHash = $"{hashStream.GetHashStringAndReset()}{command.File.FileName}{command.File.FileSize}".Sha256Base64();
}
return hash;
}
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs

@ -5,29 +5,17 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetCreatedResult : EntityCreatedResult<Guid>
public sealed class AssetCreatedResult
{
public HashSet<string> Tags { get; }
public long FileVersion { get; }
public string FileHash { get; }
public IEnrichedAssetEntity Asset { get; }
public bool IsDuplicate { get; }
public AssetCreatedResult(Guid id, HashSet<string> tags, long version, long fileVersion, string fileHash, bool isDuplicate)
: base(id, version)
public AssetCreatedResult(IEnrichedAssetEntity asset, bool isDuplicate)
{
Tags = tags;
FileVersion = fileVersion;
FileHash = fileHash;
Asset = asset;
IsDuplicate = isDuplicate;
}

82
src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs

@ -0,0 +1,82 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetEnricher : IAssetEnricher
{
private readonly ITagService tagService;
public AssetEnricher(ITagService tagService)
{
Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService;
}
public async Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset)
{
Guard.NotNull(asset, nameof(asset));
var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1));
return enriched[0];
}
public async Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets)
{
Guard.NotNull(assets, nameof(assets));
using (Profiler.TraceMethod<AssetEnricher>())
{
var results = new List<IEnrichedAssetEntity>();
foreach (var group in assets.GroupBy(x => x.AppId.Id))
{
var tagsById = await CalculateTags(group);
foreach (var asset in group)
{
var result = SimpleMapper.Map(asset, new AssetEntity());
result.TagNames = new HashSet<string>();
if (asset.Tags != null)
{
foreach (var id in asset.Tags)
{
if (tagsById.TryGetValue(id, out var name))
{
result.TagNames.Add(name);
}
}
}
results.Add(result);
}
}
return results;
}
}
private async Task<Dictionary<string, string>> CalculateTags(IGrouping<System.Guid, IAssetEntity> group)
{
var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet();
return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds);
}
}
}

9
tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs → src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs

@ -1,19 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.TestData
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class FakeAssetEntity : IAssetEntity
public sealed class AssetEntity : IEnrichedAssetEntity
{
public NamedId<Guid> AppId { get; set; }
@ -31,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public HashSet<string> Tags { get; set; }
public HashSet<string> TagNames { get; set; }
public long Version { get; set; }
public string MimeType { get; set; }

26
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -51,16 +51,27 @@ namespace Squidex.Domain.Apps.Entities.Assets
Create(c, tagIds);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
return Snapshot;
});
case UpdateAsset updateRule:
return UpdateAsync(updateRule, c =>
return UpdateReturn(updateRule, c =>
{
GuardAsset.CanUpdate(c);
Update(c);
return new AssetSavedResult(Version, Snapshot.FileVersion, Snapshot.FileHash);
return Snapshot;
});
case AnnotateAsset annotateAsset:
return UpdateReturnAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds);
return Snapshot;
});
case DeleteAsset deleteAsset:
return UpdateAsync(deleteAsset, async c =>
@ -71,15 +82,6 @@ namespace Squidex.Domain.Apps.Entities.Assets
Delete(c);
});
case AnnotateAsset annotateAsset:
return UpdateAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds);
});
default:
throw new NotSupportedException();
}

99
src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs

@ -7,7 +7,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.OData;
@ -21,9 +20,10 @@ using Squidex.Infrastructure.Queries.OData;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetQueryService : IAssetQueryService
public sealed class AssetQueryService : IAssetQueryService
{
private readonly ITagService tagService;
private readonly IAssetEnricher assetEnricher;
private readonly IAssetRepository assetRepository;
private readonly AssetOptions options;
@ -32,79 +32,85 @@ namespace Squidex.Domain.Apps.Entities.Assets
get { return options.DefaultPageSizeGraphQl; }
}
public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, IOptions<AssetOptions> options)
public AssetQueryService(
ITagService tagService,
IAssetEnricher assetEnricher,
IAssetRepository assetRepository,
IOptions<AssetOptions> options)
{
Guard.NotNull(tagService, nameof(tagService));
Guard.NotNull(options, nameof(options));
Guard.NotNull(assetEnricher, nameof(assetEnricher));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(options, nameof(options));
this.tagService = tagService;
this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository;
this.options = options.Value;
this.tagService = tagService;
}
public Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id)
{
Guard.NotNull(context, nameof(context));
return FindAssetAsync(context.App.Id, id);
}
public async Task<IAssetEntity> FindAssetAsync(Guid appId, Guid id)
public async Task<IEnrichedAssetEntity> FindAssetAsync( Guid id)
{
var asset = await assetRepository.FindAssetAsync(id);
if (asset != null)
{
await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1));
return await assetEnricher.EnrichAsync(asset);
}
return asset;
return null;
}
public async Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash)
public async Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Guid appId, string hash)
{
Guard.NotNull(hash, nameof(hash));
var assets = await assetRepository.QueryByHashAsync(appId, hash);
await DenormalizeTagsAsync(appId, assets);
return assets;
return await assetEnricher.EnrichAsync(assets);
}
public async Task<IResultList<IAssetEntity>> QueryAsync(QueryContext context, Q query)
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Q query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));
IResultList<IAssetEntity> assets;
if (query.Ids != null)
if (query.Ids != null && query.Ids.Count > 0)
{
assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids));
assets = Sort(assets, query.Ids);
assets = await QueryByIdsAsync(context, query);
}
else
{
var parsedQuery = ParseQuery(context, query.ODataQuery);
assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery);
assets = await QueryByQueryAsync(context, query);
}
await DenormalizeTagsAsync(context.App.Id, assets);
var enriched = await assetEnricher.EnrichAsync(assets);
return assets;
return ResultList.Create(assets.Total, enriched);
}
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Q query)
{
var parsedQuery = ParseQuery(context, query.ODataQuery);
return await assetRepository.QueryAsync(context.App.Id, parsedQuery);
}
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(Context context, Q query)
{
var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet<Guid>(query.Ids));
return ResultList.Create(assets.Total, sorted);
return Sort(assets, query.Ids);
}
private Query ParseQuery(QueryContext context, string query)
private static IResultList<IAssetEntity> Sort(IResultList<IAssetEntity> assets, IReadOnlyList<Guid> ids)
{
return assets.SortSet(x => x.Id, ids);
}
private Query ParseQuery(Context context, string query)
{
try
{
@ -140,34 +146,5 @@ namespace Squidex.Domain.Apps.Entities.Assets
throw new ValidationException($"Failed to parse query: {ex.Message}", ex);
}
}
private async Task DenormalizeTagsAsync(Guid appId, IEnumerable<IAssetEntity> assets)
{
var tags = new HashSet<string>(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct());
var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags);
foreach (var asset in assets)
{
if (asset.Tags?.Count > 0)
{
var tagNames = asset.Tags.ToList();
asset.Tags.Clear();
foreach (var id in tagNames)
{
if (tagsById.TryGetValue(id, out var name))
{
asset.Tags.Add(name);
}
}
}
else
{
asset.Tags?.Clear();
}
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs

@ -82,7 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
private async Task RestoreTagsAsync(Guid appId, BackupReader reader)
{
var tags = await reader.ReadJsonAttachmentAsync<TagSet>(TagsFile);
var tags = await reader.ReadJsonAttachmentAsync<TagsExport>(TagsFile);
await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags);
}

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -8,22 +8,15 @@
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class CreateAsset : AssetCommand, IAppCommand
public sealed class CreateAsset : UploadAssetCommand, IAppCommand
{
public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public HashSet<string> Tags { get; set; }
public string FileHash { get; set; }
public CreateAsset()
{
AssetId = Guid.NewGuid();

9
src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs

@ -5,16 +5,9 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class UpdateAsset : AssetCommand
public sealed class UpdateAsset : UploadAssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

20
src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class UploadAssetCommand : AssetCommand
{
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
public string FileHash { get; set; }
}
}

19
src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs

@ -0,0 +1,19 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetEnricher
{
Task<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset);
Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets);
}
}

6
src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
int DefaultPageSizeGraphQl { get; }
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(QueryContext contex, Q query);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context contex, Q query);
Task<IAssetEntity> FindAssetAsync(QueryContext context, Guid id);
Task<IEnrichedAssetEntity> FindAssetAsync(Guid id);
}
}

16
src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IEnrichedAssetEntity : IAssetEntity
{
HashSet<string> TagNames { get; }
}
}

2
src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
Task<IList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Query query);

6
src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs

@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
switch (command)
{
case CreateComment createComment:
return UpsertAsync(createComment, c =>
return UpsertReturn(createComment, c =>
{
GuardComments.CanCreate(c);
@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
});
case UpdateComment updateComment:
return UpsertAsync(updateComment, c =>
return Upsert(updateComment, c =>
{
GuardComments.CanUpdate(events, c);
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
});
case DeleteComment deleteComment:
return UpsertAsync(deleteComment, c =>
return Upsert(deleteComment, c =>
{
GuardComments.CanDelete(events, c);

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs

@ -12,7 +12,5 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public abstract class ContentDataCommand : ContentCommand
{
public NamedContentData Data { get; set; }
public bool AsDraft { get; set; }
}
}

8
src/Squidex.Web/IAppFeature.cs → src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps;
namespace Squidex.Web
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public interface IAppFeature
public abstract class ContentUpdateCommand : ContentDataCommand
{
IAppEntity App { get; }
public bool AsDraft { get; set; }
}
}

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class PatchContent : ContentDataCommand
public sealed class PatchContent : ContentUpdateCommand
{
}
}

2
src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class UpdateContent : ContentDataCommand
public sealed class UpdateContent : ContentUpdateCommand
{
}
}

7
src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -69,11 +69,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
case StatusChange.Unpublished:
result.Type = EnrichedContentEventType.Unpublished;
break;
case StatusChange.Archived:
result.Type = EnrichedContentEventType.Archived;
break;
case StatusChange.Restored:
result.Type = EnrichedContentEventType.Restored;
default:
result.Type = EnrichedContentEventType.StatusChanged;
break;
}

46
src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs

@ -0,0 +1,46 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentCommandMiddleware : GrainCommandMiddleware<ContentCommand, IContentGrain>
{
private readonly IContentEnricher contentEnricher;
public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher)
: base(grainFactory)
{
Guard.NotNull(contentEnricher, nameof(contentEnricher));
this.contentEnricher = contentEnricher;
}
public override async Task HandleAsync(CommandContext context, Func<Task> next)
{
await base.HandleAsync(context, next);
if (context.Command is SquidexCommand command && context.PlainResult is IContentEntity content && NotEnriched(context))
{
var enriched = await contentEnricher.EnrichAsync(content, command.User);
context.Complete(enriched);
}
}
private static bool NotEnriched(CommandContext context)
{
return !(context.PlainResult is IEnrichedContentEntity);
}
}
}

111
src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs

@ -0,0 +1,111 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentEnricher : IContentEnricher
{
private const string DefaultColor = StatusColors.Draft;
private readonly IContentWorkflow contentWorkflow;
private readonly IContextProvider contextProvider;
public ContentEnricher(IContentWorkflow contentWorkflow, IContextProvider contextProvider)
{
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(contextProvider, nameof(contextProvider));
this.contentWorkflow = contentWorkflow;
this.contextProvider = contextProvider;
}
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, ClaimsPrincipal user)
{
Guard.NotNull(content, nameof(content));
var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), user);
return enriched[0];
}
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, ClaimsPrincipal user)
{
Guard.NotNull(contents, nameof(contents));
Guard.NotNull(user, nameof(user));
using (Profiler.TraceMethod<ContentEnricher>())
{
var results = new List<ContentEntity>();
var cache = new Dictionary<(Guid, Status), StatusInfo>();
foreach (var content in contents)
{
var result = SimpleMapper.Map(content, new ContentEntity());
await ResolveColorAsync(content, result, cache);
if (ShouldEnrichWithStatuses())
{
await ResolveNextsAsync(content, result, user);
await ResolveCanUpdateAsync(content, result);
}
results.Add(result);
}
return results;
}
}
private bool ShouldEnrichWithStatuses()
{
return contextProvider.Context.IsFrontendClient || contextProvider.Context.IsResolveFlow();
}
private async Task ResolveCanUpdateAsync(IContentEntity content, ContentEntity result)
{
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content);
}
private async Task ResolveNextsAsync(IContentEntity content, ContentEntity result, ClaimsPrincipal user)
{
result.Nexts = await contentWorkflow.GetNextsAsync(content, user);
}
private async Task ResolveColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache)
{
result.StatusColor = await GetColorAsync(content, cache);
}
private async Task<string> GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache)
{
if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info))
{
info = await contentWorkflow.GetInfoAsync(content);
if (info == null)
{
info = new StatusInfo(content.Status, DefaultColor);
}
cache[(content.SchemaId.Id, content.Status)] = info;
}
return info.Color;
}
}
}

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

@ -8,13 +8,11 @@
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 sealed class ContentEntity : IEnrichedContentEntity
{
public Guid Id { get; set; }
@ -40,25 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public Status Status { get; set; }
public bool IsPending { get; set; }
public StatusInfo[] Nexts { get; set; }
public string StatusColor { 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;
}
public bool CanUpdate { get; set; }
public bool IsPending { get; set; }
}
}

88
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -32,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
private readonly IScriptEngine scriptEngine;
private readonly IContentWorkflow contentWorkflow;
public ContentGrain(
IStore<Guid> store,
@ -39,17 +40,20 @@ namespace Squidex.Domain.Apps.Entities.Contents
IAppProvider appProvider,
IAssetRepository assetRepository,
IScriptEngine scriptEngine,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(assetRepository, nameof(assetRepository));
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.appProvider = appProvider;
this.scriptEngine = scriptEngine;
this.assetRepository = assetRepository;
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
}
@ -64,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var ctx = await CreateContext(c.AppId.Id, c.SchemaId.Id, Guid.Empty, () => "Failed to create content.");
GuardContent.CanCreate(ctx.Schema, c);
await GuardContent.CanCreate(ctx.Schema, contentWorkflow, c);
await ctx.ExecuteScriptAndTransformAsync(s => s.Create, "Create", c, c.Data);
await ctx.EnrichAsync(c.Data);
@ -79,35 +83,43 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data);
}
Create(c);
var statusInfo = await contentWorkflow.GetInitialStatusAsync(ctx.Schema);
return EntityCreatedResult.Create(c.Data, Version);
Create(c, statusInfo.Status);
return Snapshot;
});
case UpdateContent updateContent:
return UpdateReturnAsync(updateContent, c =>
return UpdateReturnAsync(updateContent, async c =>
{
GuardContent.CanUpdate(c);
var isProposal = c.AsDraft && Snapshot.Status == Status.Published;
await GuardContent.CanUpdate(Snapshot, contentWorkflow, c, isProposal);
return UpdateAsync(c, x => c.Data, false);
return await UpdateAsync(c, x => c.Data, false, isProposal);
});
case PatchContent patchContent:
return UpdateReturnAsync(patchContent, c =>
return UpdateReturnAsync(patchContent, async c =>
{
GuardContent.CanPatch(c);
var isProposal = c.AsDraft && Snapshot.Status == Status.Published;
await GuardContent.CanPatch(Snapshot, contentWorkflow, c, isProposal);
return UpdateAsync(c, c.Data.MergeInto, true);
return await UpdateAsync(c, c.Data.MergeInto, true, isProposal);
});
case ChangeContentStatus changeContentStatus:
return UpdateAsync(changeContentStatus, async c =>
return UpdateReturnAsync(changeContentStatus, async c =>
{
try
{
var isChangeConfirm = Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published;
var ctx = await CreateContext(Snapshot.AppId.Id, Snapshot.SchemaId.Id, Snapshot.Id, () => "Failed to change content.");
GuardContent.CanChangeContentStatus(ctx.Schema, Snapshot.IsPending, Snapshot.Status, c);
await GuardContent.CanChangeStatus(ctx.Schema, Snapshot, contentWorkflow, c, isChangeConfirm);
if (c.DueTime.HasValue)
{
@ -115,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
else
{
if (Snapshot.IsPending && Snapshot.Status == Status.Published && c.Status == Status.Published)
if (isChangeConfirm)
{
ConfirmChanges(c);
}
@ -127,17 +139,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
reason = StatusChange.Published;
}
else if (c.Status == Status.Archived)
{
reason = StatusChange.Archived;
}
else if (Snapshot.Status == Status.Published)
{
reason = StatusChange.Unpublished;
}
else
{
reason = StatusChange.Restored;
reason = StatusChange.Change;
}
await ctx.ExecuteScriptAsync(s => s.Change, reason, c, Snapshot.Data);
@ -157,6 +165,18 @@ namespace Squidex.Domain.Apps.Entities.Contents
throw;
}
}
return Snapshot;
});
case DiscardChanges discardChanges:
return UpdateReturn(discardChanges, c =>
{
GuardContent.CanDiscardChanges(Snapshot.IsPending, c);
DiscardChanges(c);
return Snapshot;
});
case DeleteContent deleteContent:
@ -171,23 +191,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
Delete(c);
});
case DiscardChanges discardChanges:
return UpdateAsync(discardChanges, c =>
{
GuardContent.CanDiscardChanges(Snapshot.IsPending, c);
DiscardChanges(c);
});
default:
throw new NotSupportedException();
}
}
private async Task<object> UpdateAsync(ContentDataCommand c, Func<NamedContentData, NamedContentData> newDataFunc, bool partial)
private async Task<object> UpdateAsync(ContentUpdateCommand command, Func<NamedContentData, NamedContentData> newDataFunc, bool partial, bool isProposal)
{
var isProposal = c.AsDraft && Snapshot.Status == Status.Published;
var currentData =
isProposal ?
Snapshot.DataDraft :
@ -201,35 +211,35 @@ namespace Squidex.Domain.Apps.Entities.Contents
if (partial)
{
await ctx.ValidatePartialAsync(c.Data);
await ctx.ValidatePartialAsync(command.Data);
}
else
{
await ctx.ValidateAsync(c.Data);
await ctx.ValidateAsync(command.Data);
}
newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", c, newData, Snapshot.Data);
newData = await ctx.ExecuteScriptAndTransformAsync(s => s.Update, "Update", command, newData, Snapshot.Data);
if (isProposal)
{
ProposeUpdate(c, newData);
ProposeUpdate(command, newData);
}
else
{
Update(c, newData);
Update(command, newData);
}
}
return new ContentDataChangedResult(newData, Version);
return Snapshot;
}
public void Create(CreateContent command)
public void Create(CreateContent command, Status status)
{
RaiseEvent(SimpleMapper.Map(command, new ContentCreated()));
RaiseEvent(SimpleMapper.Map(command, new ContentCreated { Status = status }));
if (command.Publish)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published, Change = StatusChange.Published }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Status = Status.Published }));
}
}
@ -268,9 +278,9 @@ namespace Squidex.Domain.Apps.Entities.Contents
RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
}
public void ChangeStatus(ChangeContentStatus command, StatusChange reason)
public void ChangeStatus(ChangeContentStatus command, StatusChange change)
{
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = reason }));
RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged { Change = change }));
}
private void RaiseEvent(SchemaEvent @event)

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

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Microsoft.OData;
@ -23,9 +22,7 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Queries;
using Squidex.Infrastructure.Queries.OData;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
#pragma warning disable RECS0147
@ -33,14 +30,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class ContentQueryService : IContentQueryService
{
private static readonly Status[] StatusAll = { Status.Archived, Status.Draft, Status.Published };
private static readonly Status[] StatusArchived = { Status.Archived };
private static readonly Status[] StatusPublishedOnly = { Status.Published };
private static readonly Status[] StatusPublishedDraft = { Status.Published, Status.Draft };
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private static readonly IResultList<IEnrichedContentEntity> EmptyContents = ResultList.CreateFrom<IEnrichedContentEntity>(0);
private readonly IAppProvider appProvider;
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader;
private readonly IScriptEngine scriptEngine;
private readonly ContentOptions options;
private readonly EdmModelBuilder modelBuilder;
@ -53,6 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public ContentQueryService(
IAppProvider appProvider,
IAssetUrlGenerator assetUrlGenerator,
IContentEnricher contentEnricher,
IContentRepository contentRepository,
IContentVersionLoader contentVersionLoader,
IScriptEngine scriptEngine,
@ -61,6 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator));
Guard.NotNull(contentEnricher, nameof(contentEnricher));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader));
Guard.NotNull(modelBuilder, nameof(modelBuilder));
@ -69,6 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.appProvider = appProvider;
this.assetUrlGenerator = assetUrlGenerator;
this.contentEnricher = contentEnricher;
this.contentRepository = contentRepository;
this.contentVersionLoader = contentVersionLoader;
this.modelBuilder = modelBuilder;
@ -76,138 +75,126 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.scriptEngine = scriptEngine;
}
public Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName)
{
return GetSchemaAsync(context, schemaIdOrName);
}
public async Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1)
public async Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = -1)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaAsync(context, schemaIdOrName);
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
var isVersioned = version > EtagVersion.Empty;
IContentEntity content;
var status = GetFindStatus(context);
var content =
isVersioned ?
await FindContentByVersionAsync(id, version) :
await FindContentAsync(context, id, status, schema);
if (version > EtagVersion.Empty)
{
content = await FindByVersionAsync(id, version);
}
else
{
content = await FindCoreAsync(context, id, schema);
}
if (content == null || (content.Status != Status.Published && !context.IsFrontendClient) || content.SchemaId.Id != schema.Id)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity));
}
return Transform(context, schema, content);
return await TransformAsync(context, schema, content);
}
}
public async Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query)
{
Guard.NotNull(context, nameof(context));
var schema = await GetSchemaAsync(context, schemaIdOrName);
var schema = await GetSchemaOrThrowAsync(context, schemaIdOrName);
CheckPermission(context.User, schema);
CheckPermission(context, schema);
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context);
IResultList<IContentEntity> contents;
if (query.Ids?.Count > 0)
if (query.Ids != null && query.Ids.Count > 0)
{
contents = await QueryAsync(context, schema, query.Ids.ToHashSet(), status);
contents = SortSet(contents, query.Ids);
contents = await QueryByIdsAsync(context, query, schema);
}
else
{
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
contents = await QueryAsync(context, schema, parsedQuery, status);
contents = await QueryByQueryAsync(context, query, schema);
}
return Transform(context, schema, contents);
return await TransformAsync(context, schema, contents);
}
}
public async Task<IList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids)
public async Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<Guid> ids)
{
Guard.NotNull(context, nameof(context));
using (Profiler.TraceMethod<ContentQueryService>())
{
var status = GetQueryStatus(context);
List<IContentEntity> result;
if (ids?.Count > 0)
if (ids == null || ids.Count == 0)
{
var contents = await QueryAsync(context, ids, status);
return EmptyContents;
}
var permissions = context.User.Permissions();
var results = new List<IEnrichedContentEntity>();
contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList();
var contents = await QueryCoreAsync(context, ids);
result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList();
result = SortList(result, ids).ToList();
}
else
foreach (var group in contents.GroupBy(x => x.Schema.Id))
{
result = new List<IContentEntity>();
var schema = group.First().Schema;
if (HasPermission(context, schema))
{
var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content));
results.AddRange(enriched);
}
}
return result;
return ResultList.Create(results.Count, results.SortList(x => x.Id, ids));
}
}
private IResultList<IContentEntity> Transform(QueryContext context, ISchemaEntity schema, IResultList<IContentEntity> contents)
private async Task<IResultList<IEnrichedContentEntity>> TransformAsync(Context context, ISchemaEntity schema, IResultList<IContentEntity> contents)
{
var transformed = TransformCore(context, schema, contents);
var transformed = await TransformCoreAsync(context, schema, contents);
return ResultList.Create(contents.Total, transformed);
}
private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content)
{
return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault();
}
private static IResultList<IContentEntity> SortSet(IResultList<IContentEntity> contents, IReadOnlyList<Guid> ids)
private async Task<IEnrichedContentEntity> TransformAsync(Context context, ISchemaEntity schema, IContentEntity content)
{
return ResultList.Create(contents.Total, SortList(contents, ids));
}
var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1));
private static IEnumerable<IContentEntity> SortList(IEnumerable<IContentEntity> contents, IReadOnlyList<Guid> ids)
{
return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null);
return transformed[0];
}
private IEnumerable<IContentEntity> TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
private async Task<IReadOnlyList<IEnrichedContentEntity>> TransformCoreAsync(Context context, ISchemaEntity schema, IEnumerable<IContentEntity> contents)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
var results = new List<IEnrichedContentEntity>();
var converters = GenerateConverters(context).ToArray();
var scriptText = schema.SchemaDef.Scripts.Query;
var scripting = !string.IsNullOrWhiteSpace(scriptText);
var isScripting = !string.IsNullOrWhiteSpace(scriptText);
var enriched = await contentEnricher.EnrichAsync(contents, context.User);
foreach (var content in contents)
foreach (var content in enriched)
{
var result = SimpleMapper.Map(content, new ContentEntity());
if (result.Data != null)
{
if (!context.IsFrontendClient && isScripting)
if (!context.IsFrontendClient && scripting)
{
var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id };
@ -217,7 +204,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Data = result.Data.ConvertName2Name(schema.SchemaDef, converters);
}
if (result.DataDraft != null && (context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient))
if (result.DataDraft != null && (context.IsUnpublished() || context.IsFrontendClient))
{
result.DataDraft = result.DataDraft.ConvertName2Name(schema.SchemaDef, converters);
}
@ -226,12 +213,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.DataDraft = null;
}
yield return result;
results.Add(result);
}
return results;
}
}
private IEnumerable<FieldConverter> GenerateConverters(QueryContext context)
private IEnumerable<FieldConverter> GenerateConverters(Context context)
{
if (!context.IsFrontendClient)
{
@ -249,19 +238,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
yield return FieldConverters.ResolveFallbackLanguages(context.App.LanguagesConfig);
if (context.Languages?.Any() == true)
var languages = context.Languages();
if (languages.Any())
{
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, context.Languages);
yield return FieldConverters.FilterLanguages(context.App.LanguagesConfig, languages);
}
if (context.AssetUrlsToResolve?.Any() == true)
var assetUrls = context.AssetUrls();
if (assetUrls.Any() == true)
{
yield return FieldConverters.ResolveAssetUrls(context.AssetUrlsToResolve, assetUrlGenerator);
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator);
}
}
}
private Query ParseQuery(QueryContext context, string query, ISchemaEntity schema)
private Query ParseQuery(Context context, string query, ISchemaEntity schema)
{
using (Profiler.TraceMethod<ContentQueryService>())
{
@ -298,7 +291,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
public async Task<ISchemaEntity> GetSchemaAsync(QueryContext context, string schemaIdOrName)
public async Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName)
{
ISchemaEntity schema = null;
@ -320,35 +313,29 @@ namespace Squidex.Domain.Apps.Entities.Contents
return schema;
}
private static void CheckPermission(ClaimsPrincipal user, params ISchemaEntity[] schemas)
private static void CheckPermission(Context context, params ISchemaEntity[] schemas)
{
var permissions = user.Permissions();
foreach (var schema in schemas)
{
if (!HasPermission(permissions, schema))
if (!HasPermission(context, schema))
{
throw new DomainForbiddenException("You do not have permission for this schema.");
}
}
}
private static bool HasPermission(PermissionSet permissions, ISchemaEntity schema)
private static bool HasPermission(Context context, ISchemaEntity schema)
{
var permission = Permissions.ForApp(Permissions.AppContentsRead, schema.AppId.Name, schema.SchemaDef.Name);
return permissions.Allows(permission);
return context.Permissions.Allows(permission);
}
private static Status[] GetFindStatus(QueryContext context)
private static Status[] GetStatus(Context context)
{
if (context.IsFrontendClient)
{
return StatusAll;
}
else if (context.ApiStatus == StatusForApi.PublishedDraft)
if (context.IsFrontendClient || context.IsUnpublished())
{
return StatusPublishedDraft;
return null;
}
else
{
@ -356,60 +343,48 @@ namespace Squidex.Domain.Apps.Entities.Contents
}
}
private static Status[] GetQueryStatus(QueryContext context)
private async Task<IResultList<IContentEntity>> QueryByQueryAsync(Context context, Q query, ISchemaEntity schema)
{
if (context.IsFrontendClient)
{
switch (context.FrontendStatus)
{
case StatusForFrontend.Archived:
return StatusArchived;
case StatusForFrontend.PublishedOnly:
return StatusPublishedOnly;
default:
return StatusPublishedDraft;
}
}
else
{
switch (context.ApiStatus)
{
case StatusForApi.PublishedDraft:
return StatusPublishedDraft;
default:
return StatusPublishedOnly;
}
}
var parsedQuery = ParseQuery(context, query.ODataQuery, schema);
return await QueryCoreAsync(context, schema, parsedQuery);
}
private async Task<IResultList<IContentEntity>> QueryByIdsAsync(Context context, Q query, ISchemaEntity schema)
{
var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet());
return contents.SortSet(x => x.Id, query.Ids);
}
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids, Status[] status)
private Task<List<(IContentEntity Content, ISchemaEntity Schema)>> QueryCoreAsync(Context context, IReadOnlyList<Guid> ids)
{
return contentRepository.QueryAsync(context.App, status, new HashSet<Guid>(ids), ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet<Guid>(ids), WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, Query query, Status[] status)
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, Query query)
{
return contentRepository.QueryAsync(context.App, schema, status, context.IsFrontendClient, query, ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context));
}
private Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, ISchemaEntity schema, HashSet<Guid> ids, Status[] status)
private Task<IResultList<IContentEntity>> QueryCoreAsync(Context context, ISchemaEntity schema, HashSet<Guid> ids)
{
return contentRepository.QueryAsync(context.App, schema, status, ids, ShouldIncludeDraft(context));
return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context));
}
private Task<IContentEntity> FindContentAsync(QueryContext context, Guid id, Status[] status, ISchemaEntity schema)
private Task<IContentEntity> FindCoreAsync(Context context, Guid id, ISchemaEntity schema)
{
return contentRepository.FindContentAsync(context.App, schema, status, id, ShouldIncludeDraft(context));
return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context));
}
private Task<IContentEntity> FindContentByVersionAsync(Guid id, long version)
private Task<IContentEntity> FindByVersionAsync(Guid id, long version)
{
return contentVersionLoader.LoadAsync(id, version);
}
private static bool ShouldIncludeDraft(QueryContext context)
private static bool WithDraft(Context context)
{
return context.ApiStatus == StatusForApi.PublishedDraft || context.IsFrontendClient;
return context.IsUnpublished() || context.IsFrontendClient;
}
}
}

139
src/Squidex.Domain.Apps.Entities/Contents/ContextExtensions.cs

@ -0,0 +1,139 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public static class ContextExtensions
{
private const string HeaderUnpublished = "X-Unpublished";
private const string HeaderFlatten = "X-Flatten";
private const string HeaderLanguages = "X-Languages";
private const string HeaderResolveFlow = "X-ResolveFlow";
private const string HeaderResolveAssetUrls = "X-Resolve-Urls";
private static readonly char[] Separators = { ',', ';' };
public static bool IsUnpublished(this Context context)
{
return context.Headers.ContainsKey(HeaderUnpublished);
}
public static Context WithUnpublished(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderUnpublished] = "1";
}
else
{
context.Headers.Remove(HeaderUnpublished);
}
return context;
}
public static bool IsFlatten(this Context context)
{
return context.Headers.ContainsKey(HeaderFlatten);
}
public static Context WithFlatten(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderFlatten] = "1";
}
else
{
context.Headers.Remove(HeaderFlatten);
}
return context;
}
public static bool IsResolveFlow(this Context context)
{
return context.Headers.ContainsKey(HeaderResolveFlow);
}
public static Context WithResolveFlow(this Context context, bool value = true)
{
if (value)
{
context.Headers[HeaderResolveFlow] = "1";
}
else
{
context.Headers.Remove(HeaderResolveFlow);
}
return context;
}
public static IEnumerable<string> AssetUrls(this Context context)
{
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value))
{
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet();
}
return Enumerable.Empty<string>();
}
public static Context WithAssetUrlsToResolve(this Context context, IEnumerable<string> fieldNames)
{
if (fieldNames?.Any() == true)
{
context.Headers[HeaderResolveAssetUrls] = string.Join(",", fieldNames);
}
else
{
context.Headers.Remove(HeaderResolveAssetUrls);
}
return context;
}
public static IEnumerable<Language> Languages(this Context context)
{
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value))
{
var languages = new HashSet<Language>();
foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries))
{
if (Language.TryGetLanguage(iso2Code.Trim(), out var language))
{
languages.Add(language);
}
}
return languages;
}
return Enumerable.Empty<Language>();
}
public static Context WithLanguages(this Context context, IEnumerable<string> fieldNames)
{
if (fieldNames?.Any() == true)
{
context.Headers[HeaderLanguages] = string.Join(",", fieldNames);
}
else
{
context.Headers.Remove(HeaderLanguages);
}
return context;
}
}
}

96
src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -0,0 +1,96 @@
// ==========================================================================
// 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.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class DefaultContentWorkflow : IContentWorkflow
{
private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived);
private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft);
private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published);
private static readonly StatusInfo[] All =
{
InfoArchived,
InfoDraft,
InfoPublished
};
private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow =
new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)>
{
[Status.Archived] = (InfoArchived, new[]
{
InfoDraft
}),
[Status.Draft] = (InfoDraft, new[]
{
InfoArchived,
InfoPublished
}),
[Status.Published] = (InfoPublished, new[]
{
InfoDraft,
InfoArchived
})
};
public Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema)
{
var result = InfoDraft;
return Task.FromResult(result);
}
public Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user)
{
return TaskHelper.True;
}
public Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user)
{
var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next);
return Task.FromResult(result);
}
public Task<bool> CanUpdateAsync(IContentEntity content)
{
var result = content.Status != Status.Archived;
return Task.FromResult(result);
}
public Task<StatusInfo> GetInfoAsync(IContentEntity content)
{
var result = Flow[content.Status].Info;
return Task.FromResult(result);
}
public Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user)
{
var result = Flow.TryGetValue(content.Status, out var step) ? step.Transitions : Array.Empty<StatusInfo>();
return Task.FromResult(result);
}
public Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{
return Task.FromResult(All);
}
}
}

57
src/Squidex.Domain.Apps.Entities/Contents/DefaultWorkflowsValidator.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class DefaultWorkflowsValidator : IWorkflowsValidator
{
private readonly IAppProvider appProvider;
public DefaultWorkflowsValidator(IAppProvider appProvider)
{
Guard.NotNull(appProvider, nameof(appProvider));
this.appProvider = appProvider;
}
public async Task<IReadOnlyList<string>> ValidateAsync(Guid appId, Workflows workflows)
{
Guard.NotNull(workflows, nameof(workflows));
var errors = new List<string>();
if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1)
{
errors.Add("Multiple workflows cover all schemas.");
}
var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList();
foreach (var schemaId in uniqueSchemaIds)
{
if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1)
{
var schema = await appProvider.GetSchemaAsync(appId, schemaId);
if (schema != null)
{
errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows.");
}
}
}
return errors;
}
}
}

153
src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -0,0 +1,153 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
{
public sealed class DynamicContentWorkflow : IContentWorkflow
{
private readonly IScriptEngine scriptEngine;
private readonly IAppProvider appProvider;
public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider)
{
Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(appProvider, nameof(appProvider));
this.scriptEngine = scriptEngine;
this.appProvider = appProvider;
}
public async Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray();
}
public async Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user)
{
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user);
}
public async Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user);
}
public async Task<bool> CanUpdateAsync(IContentEntity content)
{
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
if (workflow.TryGetStep(content.Status, out var step))
{
return !step.NoUpdate;
}
return true;
}
public async Task<StatusInfo> GetInfoAsync(IContentEntity content)
{
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
if (workflow.TryGetStep(content.Status, out var step))
{
return new StatusInfo(content.Status, GetColor(step));
}
return new StatusInfo(content.Status, StatusColors.Draft);
}
public async Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
var (status, step) = workflow.GetInitialStep();
return new StatusInfo(status, GetColor(step));
}
public async Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user)
{
var result = new List<StatusInfo>();
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
foreach (var (to, step, transition) in workflow.GetTransitions(content.Status))
{
if (CanUse(transition, content.DataDraft, user))
{
result.Add(new StatusInfo(to, GetColor(step)));
}
}
return result.ToArray();
}
private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user)
{
if (!string.IsNullOrWhiteSpace(transition.Role))
{
if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && x.Value == transition.Role))
{
return false;
}
}
if (!string.IsNullOrWhiteSpace(transition.Expression))
{
return scriptEngine.Evaluate("data", data, transition.Expression);
}
return true;
}
private async Task<Workflow> GetWorkflowAsync(Guid appId, Guid schemaId)
{
Workflow result = null;
var app = await appProvider.GetAppAsync(appId);
if (app != null)
{
result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId));
if (result == null)
{
result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0);
}
}
if (result == null)
{
result = Workflow.Default;
}
return result;
}
private static string GetColor(WorkflowStep step)
{
return step.Color ?? StatusColors.Draft;
}
}
}

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

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.resolver = resolver;
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries)
public async Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(queries, nameof(queries));
@ -40,10 +40,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, ctx, q)));
return (result.Any(x => x.HasError), result.ToArray(x => x.Response));
return (result.Any(x => x.HasError), result.Map(x => x.Response));
}
public async Task<(bool HasError, object Response)> QueryAsync(QueryContext context, GraphQLQuery query)
public async Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));

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

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class GraphQLExecutionContext : QueryExecutionContext
{
private static readonly List<IAssetEntity> EmptyAssets = new List<IAssetEntity>();
private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IContentEntity> EmptyContents = new List<IContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver;
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public ISemanticLog Log { get; }
public GraphQLExecutionContext(QueryContext context, IDependencyResolver resolver)
public GraphQLExecutionContext(Context context, IDependencyResolver resolver)
: base(context,
resolver.Resolve<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())
@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
execution.UserContext = this;
}
public override Task<IAssetEntity> FindAssetAsync(Guid id)
public override Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{
var dataLoader = GetAssetsLoader();
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return dataLoader.LoadAsync(id);
}
public async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
public async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(IJsonValue value)
{
var ids = ParseIds(value);
@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return await dataLoader.LoadManyAsync(ids);
}
private IDataLoader<Guid, IAssetEntity> GetAssetsLoader()
private IDataLoader<Guid, IEnrichedAssetEntity> GetAssetsLoader()
{
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IAssetEntity>("Assets",
return dataLoaderContextAccessor.Context.GetOrAddBatchLoader<Guid, IEnrichedAssetEntity>("Assets",
async batch =>
{
var result = await GetReferencedAssetsAsync(new List<Guid>(batch));

4
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLService.cs

@ -11,8 +11,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public interface IGraphQLService
{
Task<(bool HasError, object Response)> QueryAsync(QueryContext context, params GraphQLQuery[] queries);
Task<(bool HasError, object Response)> QueryAsync(Context context, params GraphQLQuery[] queries);
Task<(bool HasError, object Response)> QueryAsync(QueryContext context, GraphQLQuery query);
Task<(bool HasError, object Response)> QueryAsync(Context context, GraphQLQuery query);
}
}

5
src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs

@ -7,7 +7,6 @@
using System;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
@ -28,8 +27,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType Float = new FloatGraphType();
public static readonly IGraphType Status = new EnumerationGraphType<Status>();
public static readonly IGraphType String = new StringGraphType();
public static readonly IGraphType Boolean = new BooleanGraphType();
@ -46,8 +43,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IGraphType NonNullBoolean = new NonNullGraphType(Boolean);
public static readonly IGraphType NonNullStatusType = new NonNullGraphType(Status);
public static readonly IGraphType NoopDate = new NoopGraphType(Date);
public static readonly IGraphType NoopJson = new NoopGraphType(Json);

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

@ -13,7 +13,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class AssetGraphType : ObjectGraphType<IAssetEntity>
public sealed class AssetGraphType : ObjectGraphType<IEnrichedAssetEntity>
{
public AssetGraphType(IGraphModel model)
{
@ -167,8 +167,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "tags",
ResolvedType = null,
Resolver = Resolve(x => x.Tags),
Description = "The height of the image in pixels if the asset is an image.",
Resolver = Resolve(x => x.TagNames),
Description = "The asset tags.",
Type = AllTypes.NonNullTagsType
});
@ -186,9 +186,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = "An asset";
}
private static IFieldResolver Resolve(Func<IAssetEntity, object> action)
private static IFieldResolver Resolve(Func<IEnrichedAssetEntity, object> action)
{
return new FuncFieldResolver<IAssetEntity, object>(c => action(c.Source));
return new FuncFieldResolver<IEnrichedAssetEntity, object>(c => action(c.Source));
}
}
}

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

@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public sealed class ContentGraphType : ObjectGraphType<IContentEntity>
public sealed class ContentGraphType : ObjectGraphType<IEnrichedContentEntity>
{
public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType)
{
@ -73,11 +73,19 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
AddField(new FieldType
{
Name = "status",
ResolvedType = AllTypes.NonNullStatusType,
Resolver = Resolve(x => x.Status),
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.Status.Name.ToUpperInvariant()),
Description = $"The the status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "statusColor",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.StatusColor),
Description = $"The color status of the {schemaName} content."
});
AddField(new FieldType
{
Name = "url",
@ -108,9 +116,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
Description = $"The structure of a {schemaName} content type.";
}
private static IFieldResolver Resolve(Func<IContentEntity, object> action)
private static IFieldResolver Resolve(Func<IEnrichedContentEntity, object> action)
{
return new FuncFieldResolver<IContentEntity, object>(c => action(c.Source));
return new FuncFieldResolver<IEnrichedContentEntity, object>(c => action(c.Source));
}
}
}

57
src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
@ -15,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
public static class GuardContent
{
public static void CanCreate(ISchemaEntity schema, CreateContent command)
public static async Task CanCreate(ISchemaEntity schema, IContentWorkflow contentWorkflow, CreateContent command)
{
Guard.NotNull(command, nameof(command));
@ -28,9 +29,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
throw new DomainException("Singleton content cannot be created.");
}
if (command.Publish && !await contentWorkflow.CanPublishOnCreateAsync(schema, command.Data, command.User))
{
throw new DomainException("Content workflow prevents publishing.");
}
}
public static void CanUpdate(UpdateContent command)
public static async Task CanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, UpdateContent command, bool isProposal)
{
Guard.NotNull(command, nameof(command));
@ -38,9 +44,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
ValidateData(command, e);
});
if (!isProposal)
{
await ValidateCanUpdate(content, contentWorkflow);
}
}
public static void CanPatch(PatchContent command)
public static async Task CanPatch(IContentEntity content, IContentWorkflow contentWorkflow, PatchContent command, bool isProposal)
{
Guard.NotNull(command, nameof(command));
@ -48,6 +59,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
{
ValidateData(command, e);
});
if (!isProposal)
{
await ValidateCanUpdate(content, contentWorkflow);
}
}
public static void CanDiscardChanges(bool isPending, DiscardChanges command)
@ -60,35 +76,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
}
}
public static void CanChangeContentStatus(ISchemaEntity schema, bool isPending, Status status, ChangeContentStatus command)
public static Task CanChangeStatus(ISchemaEntity schema, IContentEntity content, IContentWorkflow contentWorkflow, ChangeContentStatus command, bool isChangeConfirm)
{
Guard.NotNull(command, nameof(command));
if (schema.SchemaDef.IsSingleton && command.Status != Status.Published)
{
throw new DomainException("Singleton content archived or unpublished.");
throw new DomainException("Singleton content cannot be changed.");
}
Validate.It(() => "Cannot change status.", e =>
return Validate.It(() => "Cannot change status.", async e =>
{
if (!StatusFlow.Exists(command.Status))
{
e(Not.Valid("Status"), nameof(command.Status));
}
else if (!StatusFlow.CanChange(status, command.Status))
if (isChangeConfirm)
{
if (status == command.Status && status == Status.Published)
{
if (!isPending)
{
e("Content has no changes to publish.", nameof(command.Status));
}
}
else
if (!content.IsPending)
{
e($"Cannot change status from {status} to {command.Status}.", nameof(command.Status));
e("Content has no changes to publish.", nameof(command.Status));
}
}
else if (!await contentWorkflow.CanMoveToAsync(content, command.Status, command.User))
{
e($"Cannot change status from {content.Status} to {command.Status}.", nameof(command.Status));
}
if (command.DueTime.HasValue && command.DueTime.Value < SystemClock.Instance.GetCurrentInstant())
{
@ -114,5 +123,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
e(Not.Defined("Data"), nameof(command.Data));
}
}
private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow)
{
if (!await contentWorkflow.CanUpdateAsync(content))
{
throw new DomainException($"The workflow does not allow updates at status {content.Status}");
}
}
}
}

20
src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentEnricher
{
Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, ClaimsPrincipal user);
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, ClaimsPrincipal user);
}
}

9
src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs

@ -8,6 +8,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents
@ -16,12 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
int DefaultPageSizeGraphQl { get; }
Task<IList<IContentEntity>> QueryAsync(QueryContext context, IReadOnlyList<Guid> ids);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, IReadOnlyList<Guid> ids);
Task<IResultList<IContentEntity>> QueryAsync(QueryContext context, string schemaIdOrName, Q query);
Task<IResultList<IEnrichedContentEntity>> QueryAsync(Context context, string schemaIdOrName, Q query);
Task<IContentEntity> FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task<IEnrichedContentEntity> FindContentAsync(Context context, string schemaIdOrName, Guid id, long version = EtagVersion.Any);
Task ThrowIfSchemaNotExistsAsync(QueryContext context, string schemaIdOrName);
Task<ISchemaEntity> GetSchemaOrThrowAsync(Context context, string schemaIdOrName);
}
}

31
src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IContentWorkflow
{
Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user);
Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<StatusInfo> GetInfoAsync(IContentEntity content);
Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user);
Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema);
}
}

20
src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IEnrichedContentEntity : IContentEntity
{
bool CanUpdate { get; }
string StatusColor { get; }
StatusInfo[] Nexts { get; }
}
}

19
src/Squidex.Domain.Apps.Entities/Contents/IWorkflowsValidator.cs

@ -0,0 +1,19 @@
// ==========================================================================
// 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 Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents
{
public interface IWorkflowsValidator
{
Task<IReadOnlyList<string>> ValidateAsync(Guid appId, Workflows workflows);
}
}

12
src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs

@ -18,12 +18,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
public class QueryExecutionContext
{
private readonly ConcurrentDictionary<Guid, IContentEntity> cachedContents = new ConcurrentDictionary<Guid, IContentEntity>();
private readonly ConcurrentDictionary<Guid, IAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IAssetEntity>();
private readonly ConcurrentDictionary<Guid, IEnrichedAssetEntity> cachedAssets = new ConcurrentDictionary<Guid, IEnrichedAssetEntity>();
private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery;
private readonly QueryContext context;
private readonly Context context;
public QueryExecutionContext(QueryContext context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
{
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
@ -34,13 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.context = context;
}
public virtual async Task<IAssetEntity> FindAssetAsync(Guid id)
public virtual async Task<IEnrichedAssetEntity> FindAssetAsync(Guid id)
{
var asset = cachedAssets.GetOrDefault(id);
if (asset == null)
{
asset = await assetQuery.FindAssetAsync(context, id);
asset = await assetQuery.FindAssetAsync(id);
if (asset != null)
{
@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result;
}
public virtual async Task<IReadOnlyList<IAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
public virtual async Task<IReadOnlyList<IEnrichedAssetEntity>> GetReferencedAssetsAsync(ICollection<Guid> ids)
{
Guard.NotNull(ids, nameof(ids));

4
src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs

@ -16,12 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
public Guid Id { get; }
public Instant DueTime { get; }
public Status Status { get; }
public RefToken ScheduledBy { get; }
public Instant DueTime { get; }
public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime)
{
Id = id;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save