diff --git a/.drone.yml b/.drone.yml
index bc95a8247..d6abed99a 100644
--- a/.drone.yml
+++ b/.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:
diff --git a/Dockerfile b/Dockerfile
index e57b35d67..cabf22bc9 100644
--- a/Dockerfile
+++ b/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
diff --git a/Dockerfile.build b/Dockerfile.build
index 96debc8cd..c2002ae5c 100644
--- a/Dockerfile.build
+++ b/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
\ No newline at end of file
+RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release -p:version=$SQUIDEX__VERSION
\ No newline at end of file
diff --git a/Squidex.ruleset b/Squidex.ruleset
index 055510070..5aae5da01 100644
--- a/Squidex.ruleset
+++ b/Squidex.ruleset
@@ -63,6 +63,7 @@
+
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
index fd670011f..518ecd646 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
+++ b/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;
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
index 15ed0a51c..864961903 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
+++ b/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;
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
index 51b23d192..2a7be22e2 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
index 700fb4246..22f6f1b73 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
+++ b/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 DefaultRolesSet = new HashSet
+ private static readonly HashSet DefaultRolesSet = new HashSet(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));
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
index 58e7c5bfa..4e3e1d066 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
index a9b2bb7cb..61a90f23c 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs
new file mode 100644
index 000000000..286b83d12
--- /dev/null
+++ b/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 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);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs
new file mode 100644
index 000000000..84e8092ee
--- /dev/null
+++ b/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
+ {
+ protected override void WriteValue(JsonWriter writer, Workflows value, JsonSerializer serializer)
+ {
+ var json = new Dictionary(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>(reader);
+
+ return new Workflows(json.ToArray());
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
index c20e0c4eb..32026fc44 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
+++ b/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
{
- 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);
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
index 9e3900deb..eae462221 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
+++ b/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
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs
new file mode 100644
index 000000000..0e64ea00b
--- /dev/null
+++ b/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";
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs
new file mode 100644
index 000000000..a7ba559c7
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
deleted file mode 100644
index 005b2d4b3..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusFlow.cs
+++ /dev/null
@@ -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 Flow = new Dictionary
- {
- [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);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
similarity index 52%
rename from src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs
rename to src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
index 9f3eda547..a444badaa 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDataChangedResult.cs
+++ b/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;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
new file mode 100644
index 000000000..dae5dfd26
--- /dev/null
+++ b/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 EmptySteps = new Dictionary();
+ public static readonly IReadOnlyList EmptySchemaIds = new List();
+ public static readonly Workflow Default = CreateDefault();
+ public static readonly Workflow Empty = new Workflow(default, EmptySteps);
+
+ public IReadOnlyDictionary Steps { get; } = EmptySteps;
+
+ public IReadOnlyList SchemaIds { get; } = EmptySchemaIds;
+
+ public Status Initial { get; }
+
+ public Workflow(
+ Status initial,
+ IReadOnlyDictionary steps,
+ IReadOnlyList 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.Archived] =
+ new WorkflowStep(
+ new Dictionary
+ {
+ [Status.Draft] = new WorkflowTransition()
+ },
+ StatusColors.Archived, true),
+ [Status.Draft] =
+ new WorkflowStep(
+ new Dictionary
+ {
+ [Status.Archived] = new WorkflowTransition(),
+ [Status.Published] = new WorkflowTransition()
+ },
+ StatusColors.Draft),
+ [Status.Published] =
+ new WorkflowStep(
+ new Dictionary
+ {
+ [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]);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
new file mode 100644
index 000000000..04eb595c5
--- /dev/null
+++ b/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 EmptyTransitions = new Dictionary();
+
+ public IReadOnlyDictionary Transitions { get; }
+
+ public string Color { get; }
+
+ public bool NoUpdate { get; }
+
+ public WorkflowStep(IReadOnlyDictionary transitions = null, string color = null, bool noUpdate = false)
+ {
+ Transitions = transitions ?? EmptyTransitions;
+
+ Color = color;
+
+ NoUpdate = noUpdate;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
similarity index 50%
rename from src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs
rename to src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
index a43e109cc..a41c5ba73 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetSavedResult.cs
+++ b/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;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
new file mode 100644
index 000000000..b5d86740c
--- /dev/null
+++ b/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
+ {
+ public static readonly Workflows Empty = new Workflows();
+
+ private Workflows()
+ {
+ }
+
+ public Workflows(KeyValuePair[] 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;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs b/src/Squidex.Domain.Apps.Core.Model/Named.cs
similarity index 93%
rename from src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs
rename to src/Squidex.Domain.Apps.Core.Model/Named.cs
index 69ba9a3c1..fd76c4e8f 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/Named.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
index ed1d9b033..d9c958390 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerV2.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
index 99d62bd4e..77cf55f72 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
index f07114303..4450ef2d1 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
@@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class FieldCollection : Cloneable> where T : IField
{
public static readonly FieldCollection Empty = new FieldCollection();
-
+
private static readonly Dictionary EmptyById = new Dictionary();
private static readonly Dictionary EmptyByString = new Dictionary();
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
index 88a11c699..76ba5da7d 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
+++ b/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
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
index 78e3d3f81..3a7a90900 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
+++ b/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();
+ var nested = Children?.Map(n => n.ToNestedField()) ?? Array.Empty();
return new ArrayField(Id, Name, partitioning, nested, arrayProperties, this);
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
index 83196b881..54c31c88f 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
+++ b/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();
+ var fields = Fields.Map(f => f.ToField()) ?? Array.Empty();
var schema = new Schema(Name, fields, Properties, IsPublished, IsSingleton);
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
index b7dfb78d4..5dc24c564 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
+++ b/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
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs
index b1d672216..23989053a 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaExtensions.cs
+++ b/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
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
index 7a88572ce..f5b9cd862 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
+++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
@@ -14,6 +14,8 @@
runtime; build; native; contentfiles; analyzers
+
+
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs
index 45148a8e2..565272ef6 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/EnrichedEvents/EnrichedContentEventType.cs
+++ b/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
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
index 807210461..1d1922df0 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
+++ b/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)
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs
index 5959acc30..4cd2a9007 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs
+++ b/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;
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
index 3d2177d4b..d32a234bd 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintUser.cs
+++ b/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 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 });
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
index f0fc88a3a..ad819ba57 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs
@@ -19,11 +19,11 @@ namespace Squidex.Domain.Apps.Core.Tags
Task> DenormalizeTagsAsync(Guid appId, string group, HashSet ids);
- Task> GetTagsAsync(Guid appId, string group);
+ Task GetTagsAsync(Guid appId, string group);
- Task GetExportableTagsAsync(Guid appId, string group);
+ Task 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);
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs
similarity index 88%
rename from src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs
rename to src/Squidex.Domain.Apps.Core.Operations/Tags/TagsExport.cs
index 530c28b00..d1f54ecf7 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagSet.cs
+++ b/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
+ public sealed class TagsExport : Dictionary
{
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs b/src/Squidex.Domain.Apps.Core.Operations/Tags/TagsSet.cs
new file mode 100644
index 000000000..8e87ee8ab
--- /dev/null
+++ b/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
+ {
+ public long Version { get; set; }
+
+ public TagsSet()
+ {
+ }
+
+ public TagsSet(IDictionary tags, long version)
+ : base(tags)
+ {
+ Version = version;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
index 6444ba638..b7ccaf0ed 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
@@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
- public async Task> QueryByHashAsync(Guid appId, string hash)
+ public async Task> QueryByHashAsync(Guid appId, string hash)
{
using (Profiler.TraceMethod())
{
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
index 9c8f3eba7..fe2e0649c 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
+++ b/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]
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
index 5dd3020e7..51e7ca5a9 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
+++ b/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("QueryAsyncByQuery"))
@@ -73,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (fullTextIds?.Count == 0)
{
- return ResultList.Create(0);
+ return ResultList.CreateFrom(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> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet 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("QueryAsyncByIds"))
{
@@ -96,7 +99,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task> QueryAsync(IAppEntity app, Status[] status, HashSet ids, bool includeDraft = true)
{
Guard.NotNull(app, nameof(app));
- Guard.NotNull(status, nameof(status));
Guard.NotNull(ids, nameof(ids));
using (Profiler.TraceMethod("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())
{
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/StatusSerializer.cs
new file mode 100644
index 000000000..5d59c836a
--- /dev/null
+++ b/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
+ {
+ 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);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
index bbed5d51f..1dd0b1500 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FilterFactory.cs
+++ b/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)
{
diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs
index a227d68bf..82507f4d9 100644
--- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs
+++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs
@@ -65,6 +65,17 @@ namespace Squidex.Domain.Apps.Entities
});
}
+ public Task GetAppAsync(Guid appId)
+ {
+ return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () =>
+ {
+ using (Profiler.TraceMethod())
+ {
+ return await GetAppByIdAsync(appId);
+ }
+ });
+ }
+
public Task 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(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 GetAppByIdAsync(Guid appId)
+ {
+ var app = await grainFactory.GetGrain(appId).GetStateAsync();
+
+ if (!IsExisting(app))
+ {
+ return null;
+ }
+
+ return app.Value;
+ }
+
private async Task> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod())
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
index 63ac53ab9..95ba1cefc 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
+++ b/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()));
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
index 9b79da894..901eb1e68 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
+++ b/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(
"assigned {user:[Contributor]} as {[Role]}");
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddWorkflow.cs
new file mode 100644
index 000000000..54ca7b4bb
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs
similarity index 70%
rename from src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs
rename to src/Squidex.Domain.Apps.Entities/Apps/Commands/DeleteWorkflow.cs
index b29257b0c..c21492e79 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/StatusForFrontend.cs
+++ b/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; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateWorkflow.cs
new file mode 100644
index 000000000..635936040
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppWorkflows.cs
new file mode 100644
index 000000000..738b2f70a
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
index bf81d2612..a41e3368f 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs
+++ b/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; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
index 3327fc914..5e2454fab 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
+++ b/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>().IdOrValue;
+ return removeContributor.ContributorId;
}
private IAppsByUserIndex Index(string id)
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs
index 0bf99f271..7bde0a4cd 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs
+++ b/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 id)
+ if (assignContributor.IsCreated && context.PlainResult is IAppEntity app)
{
- context.Complete(new InvitedResult { Id = id });
+ context.Complete(new InvitedResult { App = app });
}
return;
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs b/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs
index 695be0a4b..45c6df7b9 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InvitedResult.cs
+++ b/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 Id { get; set; }
+ public IAppEntity App { get; set; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs b/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
index 0e0eddad9..f6464d5dc 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/RoleExtensions.cs
@@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{
return id;
}
- }));
+ }).Where(x => x != "common"));
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
index 4f31dbdc0..7e870d56c 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs
+++ b/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);
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
index 5c93059f2..fe8da3714 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs
@@ -21,27 +21,30 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetCommandMiddleware : GrainCommandMiddleware
{
private readonly IAssetStore assetStore;
+ private readonly IAssetEnricher assetEnricher;
private readonly IAssetQueryService assetQuery;
private readonly IAssetThumbnailGenerator assetThumbnailGenerator;
private readonly IEnumerable> tagGenerators;
public AssetCommandMiddleware(
IGrainFactory grainFactory,
+ IAssetEnricher assetEnricher,
IAssetQueryService assetQuery,
IAssetStore assetStore,
IAssetThumbnailGenerator assetThumbnailGenerator,
IEnumerable> 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();
}
- 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 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 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;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
index de8da5f23..aa932bf36 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs
+++ b/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
+ public sealed class AssetCreatedResult
{
- public HashSet Tags { get; }
-
- public long FileVersion { get; }
-
- public string FileHash { get; }
+ public IEnrichedAssetEntity Asset { get; }
public bool IsDuplicate { get; }
- public AssetCreatedResult(Guid id, HashSet 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;
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs
new file mode 100644
index 000000000..3a1b802e8
--- /dev/null
+++ b/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 EnrichAsync(IAssetEntity asset)
+ {
+ Guard.NotNull(asset, nameof(asset));
+
+ var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1));
+
+ return enriched[0];
+ }
+
+ public async Task> EnrichAsync(IEnumerable assets)
+ {
+ Guard.NotNull(assets, nameof(assets));
+
+ using (Profiler.TraceMethod())
+ {
+ var results = new List();
+
+ 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();
+
+ 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> CalculateTags(IGrouping group)
+ {
+ var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet();
+
+ return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds);
+ }
+ }
+}
diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
similarity index 86%
rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
rename to src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
index 2daf028de..150e53b78 100644
--- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs
+++ b/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 AppId { get; set; }
@@ -31,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
public HashSet Tags { get; set; }
+ public HashSet TagNames { get; set; }
+
public long Version { get; set; }
public string MimeType { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
index b16b18ae4..c76515200 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
+++ b/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();
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
index 833920248..0dc427439 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs
+++ b/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 options)
+ public AssetQueryService(
+ ITagService tagService,
+ IAssetEnricher assetEnricher,
+ IAssetRepository assetRepository,
+ IOptions 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 FindAssetAsync(QueryContext context, Guid id)
- {
- Guard.NotNull(context, nameof(context));
-
- return FindAssetAsync(context.App.Id, id);
}
- public async Task FindAssetAsync(Guid appId, Guid id)
+ public async Task 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> QueryByHashAsync(Guid appId, string hash)
+ public async Task> 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> QueryAsync(QueryContext context, Q query)
+ public async Task> QueryAsync(Context context, Q query)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(query, nameof(query));
IResultList assets;
- if (query.Ids != null)
+ if (query.Ids != null && query.Ids.Count > 0)
{
- assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(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 Sort(IResultList assets, IReadOnlyList ids)
+ private async Task> QueryByQueryAsync(Context context, Q query)
+ {
+ var parsedQuery = ParseQuery(context, query.ODataQuery);
+
+ return await assetRepository.QueryAsync(context.App.Id, parsedQuery);
+ }
+
+ private async Task> 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(query.Ids));
- return ResultList.Create(assets.Total, sorted);
+ return Sort(assets, query.Ids);
}
- private Query ParseQuery(QueryContext context, string query)
+ private static IResultList Sort(IResultList assets, IReadOnlyList 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 assets)
- {
- var tags = new HashSet(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();
- }
- }
- }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs b/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
index 068a807de..44701ee16 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
+++ b/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(TagsFile);
+ var tags = await reader.ReadJsonAttachmentAsync(TagsFile);
await tagService.RebuildTagsAsync(appId, TagGroups.Assets, tags);
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
index 9c49e67bd..8e869ba40 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
+++ b/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 AppId { get; set; }
- public AssetFile File { get; set; }
-
- public ImageInfo ImageInfo { get; set; }
-
public HashSet Tags { get; set; }
- public string FileHash { get; set; }
-
public CreateAsset()
{
AssetId = Guid.NewGuid();
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
index 1c998ac7a..16197164d 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
+++ b/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; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UploadAssetCommand.cs
new file mode 100644
index 000000000..5ef0652cd
--- /dev/null
+++ b/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; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs
new file mode 100644
index 000000000..1807af316
--- /dev/null
+++ b/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 EnrichAsync(IAssetEntity asset);
+
+ Task> EnrichAsync(IEnumerable assets);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
index 501d690a9..bec9309a6 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
@@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
int DefaultPageSizeGraphQl { get; }
- Task> QueryByHashAsync(Guid appId, string hash);
+ Task> QueryByHashAsync(Guid appId, string hash);
- Task> QueryAsync(QueryContext contex, Q query);
+ Task> QueryAsync(Context contex, Q query);
- Task FindAssetAsync(QueryContext context, Guid id);
+ Task FindAssetAsync(Guid id);
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs
new file mode 100644
index 000000000..eab0cde16
--- /dev/null
+++ b/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 TagNames { get; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
index 12de8c72a..533ce993f 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
@@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
- Task> QueryByHashAsync(Guid appId, string hash);
+ Task> QueryByHashAsync(Guid appId, string hash);
Task> QueryAsync(Guid appId, Query query);
diff --git a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs b/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
index e95b25248..e50cc05f4 100644
--- a/src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
+++ b/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);
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs
index 7f0842c16..f2eea4643 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentDataCommand.cs
+++ b/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; }
}
}
diff --git a/src/Squidex.Web/IAppFeature.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs
similarity index 69%
rename from src/Squidex.Web/IAppFeature.cs
rename to src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentUpdateCommand.cs
index a798da598..63bd8a400 100644
--- a/src/Squidex.Web/IAppFeature.cs
+++ b/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; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs
index 80206cebd..6654339d9 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/PatchContent.cs
+++ b/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
{
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs
index 01f642d5c..aeb2ce59e 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/UpdateContent.cs
+++ b/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
{
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
index dff657548..8132c6f28 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
+++ b/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;
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
new file mode 100644
index 000000000..63bc61a96
--- /dev/null
+++ b/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
+ {
+ 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 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);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs
new file mode 100644
index 000000000..32d93b04d
--- /dev/null
+++ b/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 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> EnrichAsync(IEnumerable contents, ClaimsPrincipal user)
+ {
+ Guard.NotNull(contents, nameof(contents));
+ Guard.NotNull(user, nameof(user));
+
+ using (Profiler.TraceMethod())
+ {
+ var results = new List();
+
+ 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 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;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
index 9c5a5f3f7..2e9f92115 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
+++ b/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 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; }
}
}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
index 5adf76236..60f425b5e 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
+++ b/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 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