diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..5383c8731
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,78 @@
+# Changelog
+
+## v1.1.7 - 2018-02-06
+
+### Bugfixes
+
+* **UI**: Checkbox style fixed.
+
+
+## v1.1.6 - 2018-02-06
+
+### Features
+
+* **Rules**: Allow content triggers to catch all content events.
+* **Rules**: Ensure that the events for an aggregate are handled sequentially.
+* **UI**: History stream in the dashboard.
+* **UI**: Better UI for apps overview.
+* **Apps**: Added a ready to use blog sample.
+
+### Bugfixes
+
+* **UI**: History UI was throwing an exception when a user was referenced in the message.
+* **UI**: A lot of style fixes.
+
+## v1.1.5 - 2018-02-03
+
+### Features
+
+* **Content**: Slugify function for custom scripts.
+
+### Bugfixes
+
+* **Migration**: Assets and schemas were not removed before recreation.
+* **Content**: OData queries only worked for data fields.
+* **Assets**: OData queries did not work at all and included too many fields (e.g. AppId, Id).
+
+## v1.1.4 - 2018-02-03
+
+### Features
+
+* **Login**: Consent screen to inform the user about privacy policies.
+
+## v1.1.3 - 2018-02-03
+
+### Features
+
+* **Rules**: Trigger when asset has changed
+* **Rules**: Action to purge cache items in fastly
+* **Rules**: Action to push events to Azure storage queues.
+
+### Bugfixes
+
+* **Rules**: Layout fixes.
+
+### Refactorings
+
+* Freeze action, triggers and field properties using Fody.
+* Fetch derived types automatically for Swagger generation.
+
+## v1.1.2 - 2018-01-31
+
+### Features
+
+* **Assets**: OData support, except full text search (`$search`)
+* **Rules**: Slack action
+* **Rules**: Algolia action
+
+### Bugixes
+
+* **Rules**: Color corrections for actions.
+
+### Breaking Changes
+
+* Asset structure has changed: Migration will update the ocllection automatically.
+* Asset endpoint:
+ * `take` query parameter renamed to `$top` for OData compatibility.
+ * `skip` query parameter renamed to `$skip` for OData compatibility.
+ * `query` query parameter replaced with OData. Use `$query=contains(fileName, 'MyQuery')` instead.
\ No newline at end of file
diff --git a/README.md b/README.md
index d4fed770f..9c83fa2af 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-# What is Squidex?
+# What is Squidex??
Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. We build it with ASP.NET Core and CQRS and is tested for Windows and Linux on modern browsers.
@@ -10,7 +10,7 @@ Read the docs at [https://docs.squidex.io/](https://docs.squidex.io/) (work in p
## Status
-Current Version 1.0-beta3. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
+Current Version 1.1. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites
@@ -22,7 +22,7 @@ Current Version 1.0-beta3. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadma
## Contributors
-### Core Team
+### Core Team and Founders
* [Qaisar Ahmad](http://www.qaisarahmad.com/) Interaction Designer, Pakistan
* [Sebastian Stehle](https://github.com/SebastianStehle) Software Engineer, Germany (currently Sweden)
diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings
index 235489b1a..ecdd59c86 100644
--- a/Squidex.sln.DotSettings
+++ b/Squidex.sln.DotSettings
@@ -36,12 +36,15 @@
<?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
<?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
<?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile>
+ False
==========================================================================
- $FILENAME$
Squidex Headless CMS
==========================================================================
- Copyright (c) Squidex Group
- All rights reserved.
+ Copyright (c) Squidex UG (haftungsbeschraenkt)
+ All rights reserved. Licensed under the MIT license.
==========================================================================
+ True
+ True
+ True
\ No newline at end of file
diff --git a/build.sh b/build.sh
new file mode 100644
index 000000000..5e33380b8
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,11 @@
+# Build the image
+docker build . -t squidex-build-image -f dockerfile.build
+
+# Open the image
+docker create --name squidex-build-container squidex-build-image
+
+# Copy the output to the host file system
+docker cp squidex-build-container:/out ./publish
+
+# Cleanup
+docker rm squidex-build-container
\ No newline at end of file
diff --git a/libs/keep b/libs/keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg
deleted file mode 100644
index c719a733a..000000000
Binary files a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg and /dev/null differ
diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512 b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512
deleted file mode 100644
index 11c0b5549..000000000
--- a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512
+++ /dev/null
@@ -1 +0,0 @@
-5W20j9jiNog4dHUEt+cCnePb8z6jFEMnkwO4XilajM7FCnen3KTnN/G8PAUGuQieSlTI9MRe0sRYcafLJl900w==
\ No newline at end of file
diff --git a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec b/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec
deleted file mode 100644
index d685caf66..000000000
--- a/libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- Microsoft.Orleans.OrleansCodeGenerator.Build
- 2.0.0-beta1-fix
- Microsoft Orleans Build-time Code Generator
- Microsoft
- Microsoft
- false
- true
- https://github.com/dotnet/Orleans#license
- https://github.com/dotnet/Orleans
- https://raw.githubusercontent.com/dotnet/orleans/gh-pages/assets/logo_128.png
- Microsoft Orleans build-time code generator to install in all grain interface & implementation projects.
- © Microsoft Corporation. All rights reserved.
- Orleans Cloud-Computing Actor-Model Actors Distributed-Systems C# .NET
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg
deleted file mode 100644
index a90f02b9d..000000000
Binary files a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg and /dev/null differ
diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512 b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512
deleted file mode 100644
index 6500d397d..000000000
--- a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512
+++ /dev/null
@@ -1 +0,0 @@
-mBHlGWl+bNTPP463JBEB/dftmdZKQRD8X72F7lsTFqYWddW5Ytp1gbzChCxW0d/Pt71KLF6XrVmyecbFlNdFBA==
\ No newline at end of file
diff --git a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec b/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec
deleted file mode 100644
index cf2043a3f..000000000
--- a/libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
- OrleansDashboard
- 2.0.0-beta3
- OrleansContrib
- OrleansContrib
- false
- https://opensource.org/licenses/MIT
- https://github.com/OrleansContrib/OrleansDashboard
- http://dotnet.github.io/orleans/assets/logo.png
- An admin dashboard for Microsoft Orleans
- Copyright © 2017
- orleans dashboard metrics monitor
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml b/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
new file mode 100644
index 000000000..0444e1d26
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Squidex.Infrastructure/Freezable.cs b/src/Squidex.Domain.Apps.Core.Model/Freezable.cs
similarity index 60%
rename from src/Squidex.Infrastructure/Freezable.cs
rename to src/Squidex.Domain.Apps.Core.Model/Freezable.cs
index a71d35175..9fc282430 100644
--- a/src/Squidex.Infrastructure/Freezable.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Freezable.cs
@@ -6,24 +6,30 @@
// ==========================================================================
using System;
+using Squidex.Infrastructure;
-namespace Squidex.Infrastructure
+namespace Squidex.Domain.Apps.Core
{
- public abstract class Freezable
+ public abstract class Freezable : IFreezable
{
- public bool IsFrozen { get; private set; }
+ private bool isFrozen;
- protected void ThrowIfFrozen()
+ public bool IsFrozen
{
- if (IsFrozen)
+ get { return isFrozen; }
+ }
+
+ protected void CheckIfFrozen()
+ {
+ if (isFrozen)
{
throw new InvalidOperationException("Object is frozen");
}
}
- public void Freeze()
+ public virtual void Freeze()
{
- IsFrozen = true;
+ isFrozen = true;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
new file mode 100644
index 000000000..33295be4a
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
@@ -0,0 +1,26 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Rules.Actions
+{
+ [TypeName(nameof(AlgoliaAction))]
+ public sealed class AlgoliaAction : RuleAction
+ {
+ public string AppId { get; set; }
+
+ public string ApiKey { get; set; }
+
+ public string IndexName { get; set; }
+
+ public override T Accept(IRuleActionVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
new file mode 100644
index 000000000..fc9178243
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Rules.Actions
+{
+ [TypeName(nameof(AzureQueueAction))]
+ public sealed class AzureQueueAction : RuleAction
+ {
+ public string ConnectionString { get; set; }
+
+ public string Queue { get; set; }
+
+ public override T Accept(IRuleActionVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
new file mode 100644
index 000000000..2d459d500
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Rules.Actions
+{
+ [TypeName(nameof(FastlyAction))]
+ public sealed class FastlyAction : RuleAction
+ {
+ public string ApiKey { get; set; }
+
+ public string ServiceId { get; set; }
+
+ public override T Accept(IRuleActionVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
new file mode 100644
index 000000000..b669fe104
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
@@ -0,0 +1,25 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Rules.Actions
+{
+ [TypeName(nameof(SlackAction))]
+ public sealed class SlackAction : RuleAction
+ {
+ public Uri WebhookUrl { get; set; }
+
+ public string Text { get; set; }
+
+ public override T Accept(IRuleActionVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
index 849c87a6e..30a6c0707 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
@@ -13,36 +13,9 @@ namespace Squidex.Domain.Apps.Core.Rules.Actions
[TypeName(nameof(WebhookAction))]
public sealed class WebhookAction : RuleAction
{
- private Uri url;
- private string sharedSecret;
+ public Uri Url { get; set; }
- public Uri Url
- {
- get
- {
- return url;
- }
- set
- {
- ThrowIfFrozen();
-
- url = value;
- }
- }
-
- public string SharedSecret
- {
- get
- {
- return sharedSecret;
- }
- set
- {
- ThrowIfFrozen();
-
- sharedSecret = value;
- }
- }
+ public string SharedSecret { get; set; }
public override T Accept(IRuleActionVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
index 41f68b54e..00fa80744 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
@@ -11,6 +11,14 @@ namespace Squidex.Domain.Apps.Core.Rules
{
public interface IRuleActionVisitor
{
+ T Visit(AlgoliaAction action);
+
+ T Visit(AzureQueueAction action);
+
+ T Visit(FastlyAction action);
+
+ T Visit(SlackAction action);
+
T Visit(WebhookAction action);
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
index 10d10fd0a..0793615af 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
@@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Core.Rules
{
public interface IRuleTriggerVisitor
{
+ T Visit(AssetChangedTrigger trigger);
+
T Visit(ContentChangedTrigger trigger);
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
index 02fb9e85d..caaf409d0 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
@@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using Squidex.Infrastructure;
-
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleAction : Freezable
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
index 030ff718f..963832075 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
@@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Rules
public Guid AppId { get; set; }
+ public Guid AggregateId { get; set; }
+
public string EventName { get; set; }
public string ActionName { get; set; }
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
index 1640b81ab..95c1deb0f 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
@@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using Squidex.Infrastructure;
-
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleTrigger : Freezable
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs
new file mode 100644
index 000000000..67f099873
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs
@@ -0,0 +1,28 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Rules.Triggers
+{
+ [TypeName(nameof(AssetChangedTrigger))]
+ public sealed class AssetChangedTrigger : RuleTrigger
+ {
+ public bool SendCreate { get; set; }
+
+ public bool SendUpdate { get; set; }
+
+ public bool SendRename { get; set; }
+
+ public bool SendDelete { get; set; }
+
+ public override T Accept(IRuleTriggerVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
index 2ad2d5110..e62138920 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
@@ -13,25 +13,26 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers
[TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger
{
- private ImmutableList schemas;
+ public ImmutableList Schemas { get; set; }
- public ImmutableList Schemas
- {
- get
- {
- return schemas;
- }
- set
- {
- ThrowIfFrozen();
-
- schemas = value;
- }
- }
+ public bool HandleAll { get; set; }
public override T Accept(IRuleTriggerVisitor visitor)
{
return visitor.Visit(this);
}
+
+ public override void Freeze()
+ {
+ base.Freeze();
+
+ if (Schemas != null)
+ {
+ foreach (var schema in Schemas)
+ {
+ schema.Freeze();
+ }
+ }
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
index f5e0bf7ea..33d073f5a 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
@@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Core.Rules.Triggers
{
- public sealed class ContentChangedTriggerSchema
+ public sealed class ContentChangedTriggerSchema : Freezable
{
public Guid SchemaId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
index 022890352..d3ed6270a 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
@@ -13,186 +13,29 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(AssetsField))]
public sealed class AssetsFieldProperties : FieldProperties
{
- private bool mustBeImage;
- private int? minItems;
- private int? maxItems;
- private int? minWidth;
- private int? maxWidth;
- private int? minHeight;
- private int? maxHeight;
- private int? minSize;
- private int? maxSize;
- private int? aspectWidth;
- private int? aspectHeight;
- private ImmutableList allowedExtensions;
+ public bool MustBeImage { get; set; }
- public bool MustBeImage
- {
- get
- {
- return mustBeImage;
- }
- set
- {
- ThrowIfFrozen();
+ public int? MinItems { get; set; }
- mustBeImage = value;
- }
- }
+ public int? MaxItems { get; set; }
- public int? MinItems
- {
- get
- {
- return minItems;
- }
- set
- {
- ThrowIfFrozen();
+ public int? MinWidth { get; set; }
- minItems = value;
- }
- }
+ public int? MaxWidth { get; set; }
- public int? MaxItems
- {
- get
- {
- return maxItems;
- }
- set
- {
- ThrowIfFrozen();
+ public int? MinHeight { get; set; }
- maxItems = value;
- }
- }
+ public int? MaxHeight { get; set; }
- public int? MinWidth
- {
- get
- {
- return minWidth;
- }
- set
- {
- ThrowIfFrozen();
+ public int? MinSize { get; set; }
- minWidth = value;
- }
- }
+ public int? MaxSize { get; set; }
- public int? MaxWidth
- {
- get
- {
- return maxWidth;
- }
- set
- {
- ThrowIfFrozen();
+ public int? AspectWidth { get; set; }
- maxWidth = value;
- }
- }
+ public int? AspectHeight { get; set; }
- public int? MinHeight
- {
- get
- {
- return minHeight;
- }
- set
- {
- ThrowIfFrozen();
-
- minHeight = value;
- }
- }
-
- public int? MaxHeight
- {
- get
- {
- return maxHeight;
- }
- set
- {
- ThrowIfFrozen();
-
- maxHeight = value;
- }
- }
-
- public int? MinSize
- {
- get
- {
- return minSize;
- }
- set
- {
- ThrowIfFrozen();
-
- minSize = value;
- }
- }
-
- public int? MaxSize
- {
- get
- {
- return maxSize;
- }
- set
- {
- ThrowIfFrozen();
-
- maxSize = value;
- }
- }
-
- public int? AspectWidth
- {
- get
- {
- return aspectWidth;
- }
- set
- {
- ThrowIfFrozen();
-
- aspectWidth = value;
- }
- }
-
- public int? AspectHeight
- {
- get
- {
- return aspectHeight;
- }
- set
- {
- ThrowIfFrozen();
-
- aspectHeight = value;
- }
- }
-
- public ImmutableList AllowedExtensions
- {
- get
- {
- return allowedExtensions;
- }
- set
- {
- ThrowIfFrozen();
-
- allowedExtensions = value;
- }
- }
+ public ImmutableList AllowedExtensions { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
index 06c2f2fe8..2785eceba 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
@@ -12,36 +12,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(BooleanField))]
public sealed class BooleanFieldProperties : FieldProperties
{
- private BooleanFieldEditor editor;
- private bool? defaultValue;
+ public bool? DefaultValue { get; set; }
- public bool? DefaultValue
- {
- get
- {
- return defaultValue;
- }
- set
- {
- ThrowIfFrozen();
-
- defaultValue = value;
- }
- }
-
- public BooleanFieldEditor Editor
- {
- get
- {
- return editor;
- }
- set
- {
- ThrowIfFrozen();
-
- editor = value;
- }
- }
+ public BooleanFieldEditor Editor { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
index 6456e9604..8dcdd85d5 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
@@ -13,81 +13,15 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(DateTimeField))]
public sealed class DateTimeFieldProperties : FieldProperties
{
- private DateTimeFieldEditor editor;
- private DateTimeCalculatedDefaultValue? calculatedDefaultValue;
- private Instant? maxValue;
- private Instant? minValue;
- private Instant? defaultValue;
+ public Instant? MaxValue { get; set; }
- public Instant? MaxValue
- {
- get
- {
- return maxValue;
- }
- set
- {
- ThrowIfFrozen();
-
- maxValue = value;
- }
- }
-
- public Instant? MinValue
- {
- get
- {
- return minValue;
- }
- set
- {
- ThrowIfFrozen();
-
- minValue = value;
- }
- }
-
- public Instant? DefaultValue
- {
- get
- {
- return defaultValue;
- }
- set
- {
- ThrowIfFrozen();
-
- defaultValue = value;
- }
- }
-
- public DateTimeCalculatedDefaultValue? CalculatedDefaultValue
- {
- get
- {
- return calculatedDefaultValue;
- }
- set
- {
- ThrowIfFrozen();
+ public Instant? MinValue { get; set; }
- calculatedDefaultValue = value;
- }
- }
+ public Instant? DefaultValue { get; set; }
- public DateTimeFieldEditor Editor
- {
- get
- {
- return editor;
- }
- set
- {
- ThrowIfFrozen();
+ public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; }
- editor = value;
- }
- }
+ public DateTimeFieldEditor Editor { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
index 3c0fc63f2..3820bd6c5 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
@@ -9,51 +9,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class FieldProperties : NamedElementPropertiesBase
{
- private bool isRequired;
- private bool isListField;
- private string placeholder;
+ public bool IsRequired { get; set; }
- public bool IsRequired
- {
- get
- {
- return isRequired;
- }
- set
- {
- ThrowIfFrozen();
+ public bool IsListField { get; set; }
- isRequired = value;
- }
- }
-
- public bool IsListField
- {
- get
- {
- return isListField;
- }
- set
- {
- ThrowIfFrozen();
-
- isListField = value;
- }
- }
-
- public string Placeholder
- {
- get
- {
- return placeholder;
- }
- set
- {
- ThrowIfFrozen();
-
- placeholder = value;
- }
- }
+ public string Placeholder { get; set; }
public abstract T Accept(IFieldPropertiesVisitor visitor);
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
index 985f75b5a..4cc7b239b 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
@@ -12,21 +12,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(GeolocationField))]
public sealed class GeolocationFieldProperties : FieldProperties
{
- private GeolocationFieldEditor editor;
-
- public GeolocationFieldEditor Editor
- {
- get
- {
- return editor;
- }
- set
- {
- ThrowIfFrozen();
-
- editor = value;
- }
- }
+ public GeolocationFieldEditor Editor { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
index f4f1b7497..9b3b92aba 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
@@ -5,41 +5,12 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
-using Squidex.Infrastructure;
-
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class NamedElementPropertiesBase : Freezable
{
- private string label;
- private string hints;
-
- public string Label
- {
- get
- {
- return label;
- }
- set
- {
- ThrowIfFrozen();
-
- label = value;
- }
- }
-
- public string Hints
- {
- get
- {
- return hints;
- }
- set
- {
- ThrowIfFrozen();
+ public string Label { get; set; }
- hints = value;
- }
- }
+ public string Hints { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
index cf6107222..bd86a2a74 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
@@ -13,81 +13,15 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(NumberField))]
public sealed class NumberFieldProperties : FieldProperties
{
- private double? maxValue;
- private double? minValue;
- private double? defaultValue;
- private ImmutableList allowedValues;
- private NumberFieldEditor editor;
+ public ImmutableList AllowedValues { get; set; }
- public double? MaxValue
- {
- get
- {
- return maxValue;
- }
- set
- {
- ThrowIfFrozen();
-
- maxValue = value;
- }
- }
-
- public double? MinValue
- {
- get
- {
- return minValue;
- }
- set
- {
- ThrowIfFrozen();
-
- minValue = value;
- }
- }
-
- public double? DefaultValue
- {
- get
- {
- return defaultValue;
- }
- set
- {
- ThrowIfFrozen();
-
- defaultValue = value;
- }
- }
-
- public ImmutableList AllowedValues
- {
- get
- {
- return allowedValues;
- }
- set
- {
- ThrowIfFrozen();
+ public double? MaxValue { get; set; }
- allowedValues = value;
- }
- }
+ public double? MinValue { get; set; }
- public NumberFieldEditor Editor
- {
- get
- {
- return editor;
- }
- set
- {
- ThrowIfFrozen();
+ public double? DefaultValue { get; set; }
- editor = value;
- }
- }
+ public NumberFieldEditor Editor { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
index 368ec7a2b..cc3740bda 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
@@ -13,51 +13,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(ReferencesField))]
public sealed class ReferencesFieldProperties : FieldProperties
{
- private int? minItems;
- private int? maxItems;
- private Guid schemaId;
+ public int? MinItems { get; set; }
- public int? MinItems
- {
- get
- {
- return minItems;
- }
- set
- {
- ThrowIfFrozen();
-
- minItems = value;
- }
- }
-
- public int? MaxItems
- {
- get
- {
- return maxItems;
- }
- set
- {
- ThrowIfFrozen();
+ public int? MaxItems { get; set; }
- maxItems = value;
- }
- }
-
- public Guid SchemaId
- {
- get
- {
- return schemaId;
- }
- set
- {
- ThrowIfFrozen();
-
- schemaId = value;
- }
- }
+ public Guid SchemaId { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs
index cdb515d5d..c7130e1de 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs
@@ -14,6 +14,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
Dropdown,
Radio,
RichText,
+ Slug,
TextArea
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
index 1129c646b..008c18c58 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
@@ -13,111 +13,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(StringField))]
public sealed class StringFieldProperties : FieldProperties
{
- private int? minLength;
- private int? maxLength;
- private string pattern;
- private string patternMessage;
- private string defaultValue;
- private ImmutableList allowedValues;
- private StringFieldEditor editor;
+ public ImmutableList AllowedValues { get; set; }
- public int? MinLength
- {
- get
- {
- return minLength;
- }
- set
- {
- ThrowIfFrozen();
-
- minLength = value;
- }
- }
-
- public int? MaxLength
- {
- get
- {
- return maxLength;
- }
- set
- {
- ThrowIfFrozen();
-
- maxLength = value;
- }
- }
-
- public string DefaultValue
- {
- get
- {
- return defaultValue;
- }
- set
- {
- ThrowIfFrozen();
-
- defaultValue = value;
- }
- }
+ public int? MinLength { get; set; }
- public string Pattern
- {
- get
- {
- return pattern;
- }
- set
- {
- ThrowIfFrozen();
-
- pattern = value;
- }
- }
+ public int? MaxLength { get; set; }
- public string PatternMessage
- {
- get
- {
- return patternMessage;
- }
- set
- {
- ThrowIfFrozen();
+ public string DefaultValue { get; set; }
- patternMessage = value;
- }
- }
+ public string Pattern { get; set; }
- public ImmutableList AllowedValues
- {
- get
- {
- return allowedValues;
- }
- set
- {
- ThrowIfFrozen();
+ public string PatternMessage { get; set; }
- allowedValues = value;
- }
- }
-
- public StringFieldEditor Editor
- {
- get
- {
- return editor;
- }
- set
- {
- ThrowIfFrozen();
-
- editor = value;
- }
- }
+ public StringFieldEditor Editor { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
index ab4e3b305..a87e2fd44 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
@@ -12,36 +12,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(TagsField))]
public sealed class TagsFieldProperties : FieldProperties
{
- private int? minItems;
- private int? maxItems;
+ public int? MinItems { get; set; }
- public int? MinItems
- {
- get
- {
- return minItems;
- }
- set
- {
- ThrowIfFrozen();
-
- minItems = value;
- }
- }
-
- public int? MaxItems
- {
- get
- {
- return maxItems;
- }
- set
- {
- ThrowIfFrozen();
-
- maxItems = value;
- }
- }
+ public int? MaxItems { get; set; }
public override T Accept(IFieldPropertiesVisitor visitor)
{
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 701e56aa9..91ab5c926 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
@@ -8,6 +8,8 @@
True
+
+
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
new file mode 100644
index 000000000..3a2194a9c
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
@@ -0,0 +1,157 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading.Tasks;
+using Algolia.Search;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Domain.Apps.Core.Rules.Actions;
+using Squidex.Domain.Apps.Events;
+using Squidex.Domain.Apps.Events.Contents;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Core.HandleRules.Actions
+{
+ public sealed class AlgoliaActionHandler : RuleActionHandler
+ {
+ private const string SchemaNamePlaceholder = "$SCHEMA_NAME";
+ private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients;
+ private readonly RuleEventFormatter formatter;
+
+ public AlgoliaActionHandler(RuleEventFormatter formatter)
+ {
+ Guard.NotNull(formatter, nameof(formatter));
+
+ this.formatter = formatter;
+
+ clients = new ClientPool<(string AppId, string ApiKey, string IndexName), Index>(key =>
+ {
+ var client = new AlgoliaClient(key.AppId, key.ApiKey);
+
+ return client.InitIndex(key.IndexName);
+ });
+ }
+
+ protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AlgoliaAction action)
+ {
+ var ruleDescription = string.Empty;
+ var ruleData = new RuleJobData
+ {
+ ["AppId"] = action.AppId,
+ ["ApiKey"] = action.ApiKey
+ };
+
+ if (@event.Payload is ContentEvent contentEvent)
+ {
+ ruleData["ContentId"] = contentEvent.ContentId.ToString();
+ ruleData["Operation"] = "Upsert";
+ ruleData["IndexName"] = formatter.FormatString(action.IndexName, @event);
+
+ var timestamp = @event.Headers.Timestamp().ToString();
+
+ switch (@event.Payload)
+ {
+ case ContentCreated created:
+ {
+ ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
+ ruleData["Content"] = new JObject(
+ new JProperty("id", contentEvent.ContentId),
+ new JProperty("created", timestamp),
+ new JProperty("createdBy", created.Actor.ToString()),
+ new JProperty("lastModified", timestamp),
+ new JProperty("lastModifiedBy", created.Actor.ToString()),
+ new JProperty("status", Status.Draft.ToString()),
+ new JProperty("data", formatter.ToRouteData(created.Data)));
+ break;
+ }
+
+ case ContentUpdated updated:
+ {
+ ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
+ ruleData["Content"] = new JObject(
+ new JProperty("lastModified", timestamp),
+ new JProperty("lastModifiedBy", updated.Actor.ToString()),
+ new JProperty("data", formatter.ToRouteData(updated.Data)));
+ break;
+ }
+
+ case ContentStatusChanged statusChanged:
+ {
+ ruleDescription = $"Update entry in Algolia index: {action.IndexName}";
+ ruleData["Content"] = new JObject(
+ new JProperty("status", statusChanged.Status.ToString()));
+ break;
+ }
+
+ case ContentDeleted deleted:
+ {
+ ruleDescription = $"Delete entry from Index: {action.IndexName}";
+ ruleData["Content"] = new JObject();
+ break;
+ }
+ }
+ }
+
+ return (ruleDescription, ruleData);
+ }
+
+ public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
+ {
+ if (!job.TryGetValue("Operation", out var operationToken))
+ {
+ return (null, new InvalidOperationException("The action cannot handle this event."));
+ }
+
+ var appId = job["AppId"].Value();
+ var apiKey = job["ApiKey"].Value();
+ var indexName = job["IndexName"].Value();
+
+ var index = clients.GetClient((appId, apiKey, indexName));
+
+ var operation = operationToken.Value();
+ var content = job["Content"].Value();
+ var contentId = job["ContentId"].Value();
+
+ try
+ {
+ switch (operation)
+ {
+ case "Upsert":
+ {
+ content["objectID"] = contentId;
+
+ var resonse = await index.PartialUpdateObjectAsync(content);
+
+ return (resonse.ToString(Formatting.Indented), null);
+ }
+
+ case "Delete":
+ {
+ var resonse = await index.DeleteObjectAsync(contentId);
+
+ return (resonse.ToString(Formatting.Indented), null);
+ }
+
+ default:
+ return (null, null);
+ }
+ }
+ catch (AlgoliaException ex)
+ {
+ return (ex.Message, ex);
+ }
+ catch (Exception ex)
+ {
+ return (null, ex);
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs
new file mode 100644
index 000000000..56dd3154a
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs
@@ -0,0 +1,73 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.WindowsAzure.Storage;
+using Microsoft.WindowsAzure.Storage.Queue;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Domain.Apps.Core.Rules.Actions;
+using Squidex.Domain.Apps.Events;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Core.HandleRules.Actions
+{
+ public sealed class AzureQueueActionHandler : RuleActionHandler
+ {
+ private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients;
+ private readonly RuleEventFormatter formatter;
+
+ public AzureQueueActionHandler(RuleEventFormatter formatter)
+ {
+ Guard.NotNull(formatter, nameof(formatter));
+
+ this.formatter = formatter;
+
+ clients = new ClientPool<(string ConnectionString, string QueueName), CloudQueue>(key =>
+ {
+ var storageAccount = CloudStorageAccount.Parse(key.ConnectionString);
+
+ var queueClient = storageAccount.CreateCloudQueueClient();
+ var queueRef = queueClient.GetQueueReference(key.QueueName);
+
+ return queueRef;
+ });
+ }
+
+ protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, AzureQueueAction action)
+ {
+ var body = formatter.ToRouteData(@event, eventName);
+
+ var ruleDescription = $"Send event to azure queue '{action.Queue}'";
+ var ruleData = new RuleJobData
+ {
+ ["QueueConnectionString"] = action.ConnectionString,
+ ["QueueName"] = action.Queue,
+ ["MessageBody"] = body
+ };
+
+ return (ruleDescription, ruleData);
+ }
+
+ public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
+ {
+ var queueConnectionString = job["QueueConnectionString"].Value();
+ var queueName = job["QueueName"].Value();
+
+ var queue = clients.GetClient((queueConnectionString, queueName));
+
+ var messageBody = job["MessageBody"].ToString(Formatting.Indented);
+
+ await queue.AddMessageAsync(new CloudQueueMessage(messageBody));
+
+ return ("Completed", null);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
new file mode 100644
index 000000000..5a8a252bd
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
@@ -0,0 +1,89 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Domain.Apps.Core.Rules.Actions;
+using Squidex.Domain.Apps.Events;
+using Squidex.Infrastructure.EventSourcing;
+using Squidex.Infrastructure.Http;
+
+namespace Squidex.Domain.Apps.Core.HandleRules.Actions
+{
+ public sealed class FastlyActionHandler : RuleActionHandler
+ {
+ protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, FastlyAction action)
+ {
+ var ruleDescription = "Purge key in fastly";
+ var ruleData = new RuleJobData
+ {
+ ["FastlyApiKey"] = action.ApiKey,
+ ["FastlyServiceID"] = action.ServiceId
+ };
+
+ if (@event.Headers.Contains(CommonHeaders.AggregateId))
+ {
+ ruleData["Key"] = @event.Headers.AggregateId().ToString();
+ }
+
+ return (ruleDescription, ruleData);
+ }
+
+ public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
+ {
+ if (!job.TryGetValue("Key", out var keyToken))
+ {
+ return (null, new InvalidOperationException("The action cannot handle this event."));
+ }
+
+ var requestMsg = BuildRequest(job, keyToken.Value());
+
+ HttpResponseMessage response = null;
+
+ try
+ {
+ response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
+
+ var responseString = await response.Content.ReadAsStringAsync();
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, responseString, TimeSpan.Zero, false);
+
+ return (requestDump, null);
+ }
+ catch (Exception ex)
+ {
+ if (requestMsg != null)
+ {
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, null, ex.ToString(), TimeSpan.Zero, false);
+
+ return (requestDump, ex);
+ }
+ else
+ {
+ var requestDump = ex.ToString();
+
+ return (requestDump, ex);
+ }
+ }
+ }
+
+ private static HttpRequestMessage BuildRequest(Dictionary job, string key)
+ {
+ var serviceId = job["FastlyServiceID"].Value();
+
+ var requestUrl = $"https://api.fastly.com/service/{serviceId}/purge/{key}";
+ var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
+
+ request.Headers.Add("Fastly-Key", job["FastlyApiKey"].Value());
+
+ return request;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
new file mode 100644
index 000000000..d5becff40
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
@@ -0,0 +1,99 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Domain.Apps.Core.Rules.Actions;
+using Squidex.Domain.Apps.Events;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+using Squidex.Infrastructure.Http;
+
+namespace Squidex.Domain.Apps.Core.HandleRules.Actions
+{
+ public sealed class SlackActionHandler : RuleActionHandler
+ {
+ private readonly RuleEventFormatter formatter;
+
+ public SlackActionHandler(RuleEventFormatter formatter)
+ {
+ Guard.NotNull(formatter, nameof(formatter));
+
+ this.formatter = formatter;
+ }
+
+ protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, SlackAction action)
+ {
+ var body = CreatePayload(@event, action.Text);
+
+ var ruleDescription = "Send message to slack";
+ var ruleData = new RuleJobData
+ {
+ ["RequestUrl"] = action.WebhookUrl,
+ ["RequestBody"] = body
+ };
+
+ return (ruleDescription, ruleData);
+ }
+
+ private JObject CreatePayload(Envelope @event, string text)
+ {
+ return new JObject(new JProperty("text", formatter.FormatString(text, @event)));
+ }
+
+ public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
+ {
+ var requestBody = job["RequestBody"].ToString(Formatting.Indented);
+ var requestMsg = BuildRequest(job, requestBody);
+
+ HttpResponseMessage response = null;
+
+ try
+ {
+ response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
+
+ var responseString = await response.Content.ReadAsStringAsync();
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false);
+
+ return (requestDump, null);
+ }
+ catch (Exception ex)
+ {
+ if (requestMsg != null)
+ {
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
+
+ return (requestDump, ex);
+ }
+ else
+ {
+ var requestDump = ex.ToString();
+
+ return (requestDump, ex);
+ }
+ }
+ }
+
+ private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody)
+ {
+ var requestUrl = job["RequestUrl"].Value();
+
+ var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
+ {
+ Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
+ };
+
+ return request;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
index 8ccb20c5d..9fbecd68d 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
@@ -23,24 +23,22 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{
public sealed class WebhookActionHandler : RuleActionHandler
{
- private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2);
+ private readonly RuleEventFormatter formatter;
- private readonly JsonSerializer serializer;
-
- public WebhookActionHandler(JsonSerializer serializer)
+ public WebhookActionHandler(RuleEventFormatter formatter)
{
- Guard.NotNull(serializer, nameof(serializer));
+ Guard.NotNull(formatter, nameof(formatter));
- this.serializer = serializer;
+ this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope @event, string eventName, WebhookAction action)
{
- var body = CreatePayload(@event, eventName);
+ var body = formatter.ToRouteData(@event, eventName);
var signature = $"{body.ToString(Formatting.Indented)}{action.SharedSecret}".Sha256Base64();
- var ruleDescription = $"Send event to webhook {action.Url}";
+ var ruleDescription = $"Send event to webhook '{action.Url}'";
var ruleData = new RuleJobData
{
["RequestUrl"] = action.Url,
@@ -51,38 +49,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
return (ruleDescription, ruleData);
}
- private JObject CreatePayload(Envelope @event, string eventName)
- {
- return new JObject(
- new JProperty("type", eventName),
- new JProperty("payload", JObject.FromObject(@event.Payload, serializer)),
- new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
- }
-
public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
{
var requestBody = job["RequestBody"].ToString(Formatting.Indented);
- var request = BuildRequest(job, requestBody);
+ var requestMsg = BuildRequest(job, requestBody);
HttpResponseMessage response = null;
try
{
- using (var client = new HttpClient { Timeout = Timeout })
- {
- response = await client.SendAsync(request);
+ response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
- var responseString = await response.Content.ReadAsStringAsync();
- var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString, TimeSpan.Zero, false);
+ var responseString = await response.Content.ReadAsStringAsync();
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false);
- return (requestDump, null);
- }
+ return (requestDump, null);
}
catch (Exception ex)
{
- if (request != null)
+ if (requestMsg != null)
{
- var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
+ var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, ex.ToString(), TimeSpan.Zero, false);
return (requestDump, ex);
}
@@ -97,15 +84,15 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
private static HttpRequestMessage BuildRequest(Dictionary job, string requestBody)
{
- var requestUrl = job["RequestUrl"].ToString();
- var requestSignature = job["RequestSignature"].ToString();
+ var requestUrl = job["RequestUrl"].Value();
+ var requestSig = job["RequestSignature"].Value();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
- request.Headers.Add("X-Signature", requestSignature);
+ request.Headers.Add("X-Signature", requestSig);
request.Headers.Add("User-Agent", "Squidex Webhook");
return request;
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs
new file mode 100644
index 000000000..b126e4bf3
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs
@@ -0,0 +1,39 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+
+#pragma warning disable RECS0108 // Warns about static fields in generic types
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ internal sealed class ClientPool
+ {
+ private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30);
+ private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
+ private readonly Func factory;
+
+ public ClientPool(Func factory)
+ {
+ this.factory = factory;
+ }
+
+ public TClient GetClient(TKey key)
+ {
+ if (!memoryCache.TryGetValue(key, out var client))
+ {
+ client = factory(key);
+
+ memoryCache.Set(key, client, TTL);
+ }
+
+ return client;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs
new file mode 100644
index 000000000..231920699
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs
@@ -0,0 +1,25 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public static class HttpClientPool
+ {
+ private static readonly ClientPool Pool = new ClientPool(key =>
+ {
+ return new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
+ });
+
+ public static HttpClient GetHttpClient()
+ {
+ return Pool.GetClient(string.Empty);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
new file mode 100644
index 000000000..0e64960f9
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
@@ -0,0 +1,170 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// =========================================-=================================
+
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Events;
+using Squidex.Domain.Apps.Events.Contents;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Core.HandleRules
+{
+ public class RuleEventFormatter
+ {
+ private const string Undefined = "UNDEFINED";
+ private const string AppIdPlaceholder = "$APP_ID";
+ private const string AppNamePlaceholder = "$APP_NAME";
+ private const string SchemaIdPlaceholder = "$SCHEMA_ID";
+ private const string SchemaNamePlaceholder = "$SCHEMA_NAME";
+ private const string TimestampDatePlaceholder = "$TIMESTAMP_DATE";
+ private const string TimestampDateTimePlaceholder = "$TIMESTAMP_DATETIME";
+ private const string ContentActionPlaceholder = "$CONTENT_ACTION";
+ private static readonly Regex ContentDataPlaceholder = new Regex(@"\$CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
+ private readonly JsonSerializer serializer;
+
+ public RuleEventFormatter(JsonSerializer serializer)
+ {
+ Guard.NotNull(serializer, nameof(serializer));
+
+ this.serializer = serializer;
+ }
+
+ public virtual JToken ToRouteData(object value)
+ {
+ return JToken.FromObject(value, serializer);
+ }
+
+ public virtual JToken ToRouteData(Envelope @event, string eventName)
+ {
+ return new JObject(
+ new JProperty("type", eventName),
+ new JProperty("payload", JToken.FromObject(@event.Payload, serializer)),
+ new JProperty("timestamp", @event.Headers.Timestamp().ToString()));
+ }
+
+ public virtual string FormatString(string text, Envelope @event)
+ {
+ var sb = new StringBuilder(text);
+
+ if (@event.Headers.Contains(CommonHeaders.Timestamp))
+ {
+ var timestamp = @event.Headers.Timestamp().ToDateTimeUtc();
+
+ sb.Replace(TimestampDateTimePlaceholder, timestamp.ToString("yyy-MM-dd-hh-mm-ss", CultureInfo.InvariantCulture));
+ sb.Replace(TimestampDatePlaceholder, timestamp.ToString("yyy-MM-dd", CultureInfo.InvariantCulture));
+ }
+
+ if (@event.Payload.AppId != null)
+ {
+ sb.Replace(AppIdPlaceholder, @event.Payload.AppId.Id.ToString());
+ sb.Replace(AppNamePlaceholder, @event.Payload.AppId.Name);
+ }
+
+ if (@event.Payload is SchemaEvent schemaEvent && schemaEvent.SchemaId != null)
+ {
+ sb.Replace(SchemaIdPlaceholder, schemaEvent.SchemaId.Id.ToString());
+ sb.Replace(SchemaNamePlaceholder, schemaEvent.SchemaId.Name);
+ }
+
+ FormatContentAction(@event, sb);
+
+ var result = sb.ToString();
+
+ if (@event.Payload is ContentCreated contentCreated && contentCreated.Data != null)
+ {
+ result = ReplaceData(contentCreated.Data, result);
+ }
+
+ if (@event.Payload is ContentUpdated contentUpdated && contentUpdated.Data != null)
+ {
+ result = ReplaceData(contentUpdated.Data, result);
+ }
+
+ return result;
+ }
+
+ private static void FormatContentAction(Envelope @event, StringBuilder sb)
+ {
+ switch (@event.Payload)
+ {
+ case ContentCreated contentCreated:
+ sb.Replace(ContentActionPlaceholder, "created");
+ break;
+
+ case ContentUpdated contentUpdated:
+ sb.Replace(ContentActionPlaceholder, "updated");
+ break;
+
+ case ContentStatusChanged contentStatusChanged:
+ sb.Replace(ContentActionPlaceholder, $"set to {contentStatusChanged.Status.ToString().ToLowerInvariant()}");
+ break;
+
+ case ContentDeleted contentDeleted:
+ sb.Replace(ContentActionPlaceholder, "deleted");
+ break;
+ }
+ }
+
+ private static string ReplaceData(NamedContentData data, string text)
+ {
+ return ContentDataPlaceholder.Replace(text, match =>
+ {
+ var captures = match.Groups[2].Captures;
+
+ var path = new string[captures.Count];
+
+ for (var i = 0; i < path.Length; i++)
+ {
+ path[i] = captures[i].Value;
+ }
+
+ if (!data.TryGetValue(path[0], out var field))
+ {
+ return Undefined;
+ }
+
+ if (!field.TryGetValue(path[1], out var value))
+ {
+ return Undefined;
+ }
+
+ for (var j = 2; j < path.Length; j++)
+ {
+ if (value is JObject obj && obj.TryGetValue(path[j], out value))
+ {
+ continue;
+ }
+ if (value is JArray arr && int.TryParse(path[j], out var idx) && idx >= 0 && idx < arr.Count)
+ {
+ value = arr[idx];
+ }
+ else
+ {
+ return Undefined;
+ }
+ }
+
+ if (value == null || value.Type == JTokenType.Null || value.Type == JTokenType.Undefined)
+ {
+ return Undefined;
+ }
+
+ if (value is JValue jValue && jValue != null)
+ {
+ return jValue.Value.ToString();
+ }
+
+ return value?.ToString(Formatting.Indented) ?? Undefined;
+ });
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
index 4b1be3d3e..ecac1cf7a 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
@@ -87,16 +87,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
@event.Headers.Timestamp() :
now;
- var eventGuid =
- @event.Headers.Contains(CommonHeaders.EventId) ?
- @event.Headers.EventId() :
+ var aggregateId =
+ @event.Headers.Contains(CommonHeaders.AggregateId) ?
+ @event.Headers.AggregateId() :
Guid.NewGuid();
var job = new RuleJob
{
- JobId = eventGuid,
+ JobId = Guid.NewGuid(),
ActionName = actionName,
ActionData = actionData.Data,
+ AggregateId = aggregateId,
AppId = appEvent.AppId.Id,
Created = now,
EventName = eventName,
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs
new file mode 100644
index 000000000..22555b6e2
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs
@@ -0,0 +1,31 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Squidex.Domain.Apps.Core.Rules.Triggers;
+using Squidex.Domain.Apps.Events;
+using Squidex.Domain.Apps.Events.Assets;
+using Squidex.Infrastructure.EventSourcing;
+
+namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
+{
+ public sealed class AssetChangedTriggerHandler : RuleTriggerHandler
+ {
+ protected override bool Triggers(Envelope @event, AssetChangedTrigger trigger)
+ {
+ return @event.Payload is AssetEvent assetEvent && MatchsType(trigger, assetEvent);
+ }
+
+ private static bool MatchsType(AssetChangedTrigger trigger, AssetEvent @event)
+ {
+ return
+ (trigger.SendCreate && @event is AssetCreated) ||
+ (trigger.SendUpdate && @event is AssetUpdated) ||
+ (trigger.SendDelete && @event is AssetDeleted) ||
+ (trigger.SendRename && @event is AssetRenamed);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
index 2194abae6..13ca5f459 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
@@ -17,6 +17,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers
{
protected override bool Triggers(Envelope @event, ContentChangedTrigger trigger)
{
+ if (trigger.HandleAll)
+ {
+ return true;
+ }
+
if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent)
{
foreach (var schema in trigger.Schemas)
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
index 934ee5387..79b1e14c4 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
@@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
EnsurePropertiesInitialized();
- return fieldProperties.GetOrDefault(propertyName) ?? new PropertyDescriptor(new ObjectInstance(Engine) { Extensible = true }, true, false, true);
+ return fieldProperties.GetOrAdd(propertyName, x => new ContentDataProperty(this, new ContentFieldObject(this, new ContentFieldData(), false)));
}
public override IEnumerable> GetOwnProperties()
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
index 943d41240..4ea31b9ea 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
@@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{
if (value == null || !value.IsObject())
{
- throw new JavaScriptException("Can only assign object to content data.");
+ throw new JavaScriptException("You can only assign objects to content data.");
}
var obj = value.AsObject();
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
index 5e0772860..57e135981 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
+++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
@@ -115,11 +115,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
catch (ParserException ex)
{
- throw new ValidationException("Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message));
+ throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
- throw new ValidationException("Failed to execute script with javascript error.", new ValidationError(ex.Message));
+ throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
}
}
@@ -150,6 +150,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
engine.SetValue("ctx", contextInstance);
+ engine.SetValue("slugify", new Func(x => x.Slugify()));
return engine;
}
diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
index d69665a81..8a5057653 100644
--- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
+++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
@@ -13,15 +13,17 @@
+
-
+
-
+
-
+
+
..\..\Squidex.ruleset
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
index 7a39e0329..2f2729858 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
@@ -8,24 +8,74 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
-using Squidex.Domain.Apps.Entities.Assets.State;
+using Squidex.Domain.Apps.Core.ValidateContent;
+using Squidex.Domain.Apps.Entities.Assets;
+using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
- public sealed class MongoAssetEntity : IVersionedEntity
+ public sealed class MongoAssetEntity :
+ MongoEntity,
+ IAssetEntity,
+ IUpdateableEntityWithVersion,
+ IUpdateableEntityWithCreatedBy,
+ IUpdateableEntityWithLastModifiedBy
{
- [BsonId]
+ [BsonRequired]
+ [BsonElement]
+ public Guid AppIdId { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public NamedId AppId { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public string MimeType { get; set; }
+
+ [BsonRequired]
[BsonElement]
- [BsonRepresentation(BsonType.String)]
- public Guid Id { get; set; }
+ public string FileName { get; set; }
+ [BsonRequired]
[BsonElement]
+ public long FileSize { get; set; }
+
[BsonRequired]
- public AssetState State { get; set; }
+ [BsonElement]
+ public long FileVersion { get; set; }
+ [BsonRequired]
[BsonElement]
+ public bool IsImage { get; set; }
+
[BsonRequired]
+ [BsonElement]
public long Version { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public int? PixelWidth { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public int? PixelHeight { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public RefToken CreatedBy { get; set; }
+
+ [BsonRequired]
+ [BsonElement]
+ public RefToken LastModifiedBy { get; set; }
+
+ [BsonElement]
+ public bool IsDeleted { get; set; }
+
+ Guid IAssetInfo.AssetId
+ {
+ get { return Id; }
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
index 1b3abce9c..737c7288c 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
@@ -9,10 +9,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
+using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
+using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
@@ -34,44 +35,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
return collection.Indexes.CreateOneAsync(
Index
- .Ascending(x => x.State.AppId)
- .Ascending(x => x.State.IsDeleted)
- .Ascending(x => x.State.FileName)
- .Ascending(x => x.State.MimeType)
- .Descending(x => x.State.LastModified));
+ .Ascending(x => x.AppId)
+ .Ascending(x => x.IsDeleted)
+ .Ascending(x => x.FileName)
+ .Descending(x => x.LastModified));
}
- public async Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0)
+ public async Task> QueryAsync(Guid appId, string query = null)
{
- var filters = new List>
+ try
{
- Filter.Eq(x => x.State.AppId, appId),
- Filter.Eq(x => x.State.IsDeleted, false)
- };
+ var odataQuery = EdmAssetModel.Edm.ParseQuery(query);
- if (ids != null && ids.Count > 0)
+ var filter = FindExtensions.BuildQuery(odataQuery, appId);
+
+ var contentCount = Collection.Find(filter).CountAsync();
+ var contentItems =
+ Collection.Find(filter)
+ .AssetTake(odataQuery)
+ .AssetSkip(odataQuery)
+ .AssetSort(odataQuery)
+ .ToListAsync();
+
+ await Task.WhenAll(contentItems, contentCount);
+
+ return ResultList.Create(contentItems.Result, contentCount.Result);
+ }
+ catch (NotSupportedException)
{
- filters.Add(Filter.In(x => x.Id, ids));
+ throw new ValidationException("This odata operation is not supported.");
}
-
- if (mimeTypes != null && mimeTypes.Count > 0)
+ catch (NotImplementedException)
{
- filters.Add(Filter.In(x => x.State.MimeType, mimeTypes));
+ throw new ValidationException("This odata operation is not supported.");
}
-
- if (!string.IsNullOrWhiteSpace(query))
+ catch (MongoQueryException ex)
{
- filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i")));
+ if (ex.Message.Contains("17406"))
+ {
+ throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
+ }
+ else
+ {
+ throw;
+ }
}
+ }
- var filter = Filter.And(filters);
+ public async Task> QueryAsync(Guid appId, HashSet ids)
+ {
+ var find = Collection.Find(Filter.In(x => x.Id, ids)).SortByDescending(x => x.LastModified);
- var assetItems = Collection.Find(filter).Skip(skip).Limit(take).SortByDescending(x => x.State.LastModified).ToListAsync();
- var assetCount = Collection.Find(filter).CountAsync();
+ var assetItems = find.ToListAsync();
+ var assetCount = find.CountAsync();
await Task.WhenAll(assetItems, assetCount);
- return ResultList.Create(assetItems.Result.Select(x => x.State), assetCount.Result);
+ return ResultList.Create(assetItems.Result.OfType().ToList(), assetCount.Result);
}
public async Task FindAssetAsync(Guid id)
@@ -80,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync();
- return assetEntity?.State;
+ return assetEntity;
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
index d181e97ca..a4056d0a7 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
@@ -10,7 +10,7 @@ using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure;
-using Squidex.Infrastructure.MongoDb;
+using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
@@ -25,15 +25,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null)
{
- return (existing.State, existing.Version);
+ return (SimpleMapper.Map(existing, new AssetState()), existing.Version);
}
return (null, EtagVersion.NotFound);
}
- public Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
+ public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
- return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.State, value));
+ var entity = SimpleMapper.Map(value, new MongoAssetEntity());
+
+ entity.Version = newVersion;
+ entity.AppIdId = value.AppId.Id;
+
+ await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
new file mode 100644
index 000000000..af3a75764
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
@@ -0,0 +1,86 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using Microsoft.OData.UriParser;
+using MongoDB.Bson;
+using MongoDB.Driver;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.MongoDb.OData;
+
+namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
+{
+ public static class FindExtensions
+ {
+ private static readonly FilterDefinitionBuilder Filter = Builders.Filter;
+ private static readonly PropertyCalculator PropertyCalculator = propertyNames =>
+ {
+ if (propertyNames.Length > 0)
+ {
+ propertyNames[0] = propertyNames[0].ToPascalCase();
+ }
+
+ var propertyName = string.Join(".", propertyNames);
+
+ return propertyName;
+ };
+
+ public static IFindFluent AssetSort(this IFindFluent cursor, ODataUriParser query)
+ {
+ var sort = query.BuildSort(PropertyCalculator);
+
+ return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
+ }
+
+ public static IFindFluent AssetTake(this IFindFluent cursor, ODataUriParser query)
+ {
+ return cursor.Take(query, 200, 20);
+ }
+
+ public static IFindFluent AssetSkip(this IFindFluent cursor, ODataUriParser query)
+ {
+ return cursor.Skip(query);
+ }
+
+ public static FilterDefinition BuildQuery(ODataUriParser query, Guid appId)
+ {
+ var filters = new List>
+ {
+ Filter.Eq(x => x.AppIdId, appId),
+ Filter.Eq(x => x.IsDeleted, false)
+ };
+
+ var filter = query.BuildFilter(PropertyCalculator, false);
+
+ if (filter.Filter != null)
+ {
+ if (filter.Last)
+ {
+ filters.Add(filter.Filter);
+ }
+ else
+ {
+ filters.Insert(0, filter.Filter);
+ }
+ }
+
+ if (filters.Count > 1)
+ {
+ return Filter.And(filters);
+ }
+ else if (filters.Count == 1)
+ {
+ return filters[0];
+ }
+ else
+ {
+ return new BsonDocument();
+ }
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
index 68d92be78..f35cb2286 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
@@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired]
[BsonElement("ai")]
[BsonRepresentation(BsonType.String)]
- public Guid AppId { get; set; }
+ public Guid AppIdId { get; set; }
[BsonRequired]
[BsonElement("si")]
[BsonRepresentation(BsonType.String)]
- public Guid SchemaId { get; set; }
+ public Guid SchemaIdId { get; set; }
[BsonRequired]
[BsonElement("rf")]
@@ -62,6 +62,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonJson]
public IdContentData DataByIds { get; set; }
+ [BsonRequired]
+ [BsonElement("ai2")]
+ public NamedId AppId { get; set; }
+
+ [BsonRequired]
+ [BsonElement("si2")]
+ public NamedId SchemaId { get; set; }
+
+ [BsonIgnoreIfNull]
+ [BsonElement("sdt")]
+ public Status? ScheduledTo { get; set; }
+
+ [BsonIgnoreIfNull]
+ [BsonElement("sda")]
+ public Instant? ScheduledAt { get; set; }
+
+ [BsonIgnoreIfNull]
+ [BsonElement("sdb")]
+ public RefToken ScheduledBy { get; set; }
+
[BsonRequired]
[BsonElement("ct")]
public Instant Created { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
index eba2e0b88..1d4cc508f 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
@@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.OData.UriParser;
using MongoDB.Driver;
+using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
@@ -49,6 +50,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override async Task SetupCollectionAsync(IMongoCollection collection)
{
+ await collection.Indexes.TryDropOneAsync("si_1_st_1_dl_1_dt_text");
+
+ await archiveCollection.Indexes.CreateOneAsync(
+ Index
+ .Ascending(x => x.ScheduledTo));
+
await archiveCollection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.Id)
@@ -56,30 +63,45 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateOneAsync(
Index
- .Ascending(x => x.SchemaId)
+ .Text(x => x.DataText)
+ .Ascending(x => x.SchemaIdId)
.Ascending(x => x.Status)
- .Ascending(x => x.IsDeleted)
- .Text(x => x.DataText));
-
- await collection.Indexes.CreateOneAsync(
- Index
- .Ascending(x => x.Id)
.Ascending(x => x.IsDeleted));
await collection.Indexes.CreateOneAsync(
Index
+ .Ascending(x => x.SchemaIdId)
.Ascending(x => x.Id)
- .Ascending(x => x.Version));
+ .Ascending(x => x.IsDeleted)
+ .Ascending(x => x.Status));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
}
public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
{
- FilterDefinition filter;
try
{
- filter = FindExtensions.BuildQuery(odataQuery, schema.Id, schema.SchemaDef, status);
+ var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef);
+
+ var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator);
+
+ var contentCount = Collection.Find(filter).CountAsync();
+ var contentItems =
+ Collection.Find(filter)
+ .ContentTake(odataQuery)
+ .ContentSkip(odataQuery)
+ .ContentSort(odataQuery, propertyCalculator)
+ .ToListAsync();
+
+ await Task.WhenAll(contentItems, contentCount);
+
+ foreach (var entity in contentItems.Result)
+ {
+ entity.ParseData(schema.SchemaDef);
+ }
+
+ return ResultList.Create(contentItems.Result, contentCount.Result);
}
catch (NotSupportedException)
{
@@ -89,23 +111,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
throw new ValidationException("This odata operation is not supported.");
}
-
- var contentItems = Collection.Find(filter).Take(odataQuery).Skip(odataQuery).Sort(odataQuery, schema.SchemaDef).ToListAsync();
- var contentCount = Collection.Find(filter).CountAsync();
-
- await Task.WhenAll(contentItems, contentCount);
-
- foreach (var entity in contentItems.Result)
+ catch (MongoQueryException ex)
{
- entity.ParseData(schema.SchemaDef);
+ if (ex.Message.Contains("17406"))
+ {
+ throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
+ }
+ else
+ {
+ throw;
+ }
}
-
- return ResultList.Create(contentItems.Result, contentCount.Result);
}
public async Task> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet ids)
{
- var find = Collection.Find(x => ids.Contains(x.Id));
+ var find = Collection.Find(x => x.SchemaIdId == schema.Id && ids.Contains(x.Id) && x.IsDeleted == false && status.Contains(x.Status));
var contentItems = find.ToListAsync();
var contentCount = find.CountAsync();
@@ -120,13 +141,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ResultList.Create(contentItems.Result, contentCount.Result);
}
- public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList contentIds)
+ public async Task> QueryNotFoundAsync(Guid appId, Guid schemaId, IList ids)
{
var contentEntities =
- await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id)
+ await Collection.Find(x => x.SchemaIdId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id)
.ToListAsync();
- return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList();
+ return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList();
}
public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version)
@@ -143,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
{
var contentEntity =
- await Collection.Find(x => x.Id == id && !x.IsDeleted)
+ await Collection.Find(x => x.SchemaIdId == schema.Id && x.Id == id && x.IsDeleted == false)
.FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef);
@@ -151,6 +172,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contentEntity;
}
+ public Task QueryScheduledWithoutDataAsync(Instant now, Func callback)
+ {
+ return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted == false)
+ .ForEachAsync(c =>
+ {
+ callback(c);
+ });
+ }
+
public override async Task ClearAsync()
{
await Database.DropCollectionAsync("States_Contents_Archive");
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
index 2f787186e..8a923d361 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
@@ -10,6 +10,7 @@ using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
+using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
@@ -47,5 +48,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
}
+
+ Task IEventConsumer.ClearAsync()
+ {
+ return TaskHelper.Done;
+ }
}
}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
index c220e416e..54e236e4a 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
@@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (contentEntity != null)
{
- var schema = await GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId);
+ var schema = await GetSchemaAsync(contentEntity.AppIdId, contentEntity.SchemaIdId);
contentEntity?.ParseData(schema.SchemaDef);
@@ -40,12 +40,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
{
- if (value.SchemaId == Guid.Empty)
+ if (value.SchemaId.Id == Guid.Empty)
{
return;
}
- var schema = await GetSchemaAsync(value.AppId, value.SchemaId);
+ var schema = await GetSchemaAsync(value.AppId.Id, value.SchemaId.Id);
var idData = value.Data?.ToIdModel(schema.SchemaDef, true);
@@ -53,6 +53,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
var document = SimpleMapper.Map(value, new MongoContentEntity
{
+ AppIdId = value.AppId.Id,
+ SchemaIdId = value.SchemaId.Id,
IsDeleted = value.IsDeleted,
DocumentId = key.ToString(),
DataText = idData?.ToFullText(),
@@ -92,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private async Task GetSchemaAsync(Guid appId, Guid schemaId)
{
- var schema = await appProvider.GetSchemaAsync(appId, schemaId);
+ var schema = await appProvider.GetSchemaAsync(appId, schemaId, true);
if (schema == null)
{
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
index 606cd1a28..cdfaff9b8 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
@@ -7,10 +7,15 @@
using System;
using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
using Microsoft.OData.UriParser;
+using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
+using Squidex.Infrastructure.MongoDb.OData;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
@@ -18,64 +23,80 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
private static readonly FilterDefinitionBuilder Filter = Builders.Filter;
- public static IFindFluent Sort(this IFindFluent cursor, ODataUriParser query, Schema schema)
+ private static readonly Dictionary PropertyMap =
+ typeof(MongoContentEntity).GetProperties()
+ .ToDictionary(x => x.Name, x => x.GetCustomAttribute()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase);
+
+ static FindExtensions()
{
- return cursor.Sort(SortBuilder.BuildSort(query, schema));
+ PropertyMap["Data"] = "do";
}
- public static IFindFluent Take(this IFindFluent cursor, ODataUriParser query)
+ public static PropertyCalculator CreatePropertyCalculator(Schema schema)
{
- var top = query.ParseTop();
-
- if (top.HasValue)
- {
- cursor = cursor.Limit(Math.Min((int)top.Value, 200));
- }
- else
+ return propertyNames =>
{
- cursor = cursor.Limit(20);
- }
+ if (propertyNames.Length > 1)
+ {
+ var edmName = propertyNames[1].UnescapeEdmField();
+
+ if (!schema.FieldsByName.TryGetValue(edmName, out var field))
+ {
+ throw new NotSupportedException();
+ }
+
+ propertyNames[1] = field.Id.ToString();
+ }
+
+ if (propertyNames.Length > 0)
+ {
+ propertyNames[0] = PropertyMap[propertyNames[0]];
+ }
+
+ var propertyName = string.Join(".", propertyNames);
- return cursor;
+ return propertyName;
+ };
}
- public static IFindFluent Skip(this IFindFluent cursor, ODataUriParser query)
+ public static IFindFluent ContentSort(this IFindFluent cursor, ODataUriParser query, PropertyCalculator propertyCalculator)
{
- var skip = query.ParseSkip();
+ var sort = query.BuildSort(propertyCalculator);
- if (skip.HasValue)
- {
- cursor = cursor.Skip((int)skip.Value);
- }
- else
- {
- cursor = cursor.Skip(null);
- }
-
- return cursor;
+ return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
}
- public static IFindFluent Find(this IMongoCollection cursor, ODataUriParser query, Guid schemaId, Schema schema, Status[] status)
+ public static IFindFluent ContentTake(this IFindFluent cursor, ODataUriParser query)
{
- var filter = BuildQuery(query, schemaId, schema, status);
+ return cursor.Take(query, 200, 20);
+ }
- return cursor.Find(filter);
+ public static IFindFluent ContentSkip(this IFindFluent cursor, ODataUriParser query)
+ {
+ return cursor.Skip(query);
}
- public static FilterDefinition BuildQuery(ODataUriParser query, Guid schemaId, Schema schema, Status[] status)
+ public static FilterDefinition BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, PropertyCalculator propertyCalculator)
{
var filters = new List>
{
- Filter.Eq(x => x.SchemaId, schemaId),
+ Filter.Eq(x => x.SchemaIdId, schemaId),
Filter.In(x => x.Status, status),
Filter.Eq(x => x.IsDeleted, false)
};
- var filter = FilterBuilder.Build(query, schema);
+ var filter = query.BuildFilter(propertyCalculator);
- if (filter != null)
+ if (filter.Filter != null)
{
- filters.Add(filter);
+ if (filter.Last)
+ {
+ filters.Add(filter.Filter);
+ }
+ else
+ {
+ filters.Insert(0, filter.Filter);
+ }
}
if (filters.Count == 1)
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs
index 610d38977..5e9d9d50d 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs
@@ -16,11 +16,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
{
public sealed class MongoHistoryEventEntity : MongoEntity,
IEntity,
- IEntityWithAppRef,
IUpdateableEntity,
IUpdateableEntityWithVersion,
- IUpdateableEntityWithCreatedBy,
- IUpdateableEntityWithAppRef
+ IUpdateableEntityWithCreatedBy
{
[BsonElement]
[BsonRequired]
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
index dd76dbd6b..55ce5a471 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
@@ -66,9 +66,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
public async Task> QueryByChannelAsync(Guid appId, string channelPrefix, int count)
{
- var historyEventEntities =
- await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
- .ToListAsync();
+ List historyEventEntities;
+
+ if (!string.IsNullOrWhiteSpace(channelPrefix))
+ {
+ historyEventEntities =
+ await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
+ .ToListAsync();
+ }
+ else
+ {
+ historyEventEntities =
+ await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
+ .ToListAsync();
+ }
return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs
index 408a3aaba..cd8a2ee02 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs
@@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
{
return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u
.Set(x => x.State, value)
- .Set(x => x.AppId, value.AppId)
+ .Set(x => x.AppId, value.AppId.Id)
.Set(x => x.IsDeleted, value.IsDeleted));
}
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs
index aadd8cbcb..a23899a7a 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs
@@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u
.Set(x => x.State, value)
- .Set(x => x.AppId, value.AppId)
+ .Set(x => x.AppId, value.AppId.Id)
.Set(x => x.Name, value.Name)
.Set(x => x.IsDeleted, value.IsDeleted));
}
diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
index bce4941ad..1095818b8 100644
--- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
+++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
@@ -15,9 +15,9 @@
-
+
-
+
diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs
index 38f0b2742..b2e028a12 100644
--- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs
+++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs
@@ -88,11 +88,11 @@ namespace Squidex.Domain.Apps.Entities
return (await stateFactory.GetSingleAsync(schemaId)).Snapshot;
}
- public async Task GetSchemaAsync(Guid appId, Guid id)
+ public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{
var schema = await stateFactory.GetSingleAsync(id);
- if (!IsFound(schema))
+ if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId)
{
return null;
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
index 51c460837..829c1388c 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
@@ -45,9 +45,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.appPlansBillingManager = appPlansBillingManager;
}
- protected Task On(CreateApp command, CommandContext context)
+ protected async Task On(CreateApp command, CommandContext context)
{
- return handler.CreateSyncedAsync(context, async a =>
+ var app = await handler.CreateSyncedAsync(context, async a =>
{
await GuardApp.CanCreate(command, appProvider);
@@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
else
{
- var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, command.AppId.Id, a.Snapshot.Name, command.PlanId);
+ var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Snapshot.Id, a.Snapshot.Name, command.PlanId);
if (result is PlanChangedResult)
{
@@ -193,10 +193,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task HandleAsync(CommandContext context, Func next)
{
- if (!await this.DispatchActionAsync(context.Command, context))
- {
- await next();
- }
+ await this.DispatchActionAsync(context.Command, context);
+ await next();
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
index f22d2ef2f..e722f8d06 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
@@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
protected override Task CreateEventCoreAsync(Envelope @event)
{
- return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null);
+ return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);
}
private static string ClientName(AppClientRenamed @event)
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs
index b2619a6f6..3cfec0965 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class AddLanguage : AppAggregateCommand
+ public sealed class AddLanguage : AppCommand
{
public Language Language { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
index 5442536c1..30873adbb 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
@@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class AddPattern : AppAggregateCommand
+ public sealed class AddPattern : AppCommand
{
public Guid PatternId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs
similarity index 71%
rename from src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs
rename to src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs
index d3d73dd62..a391da077 100644
--- a/src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs
@@ -8,13 +8,15 @@
using System;
using Squidex.Infrastructure.Commands;
-namespace Squidex.Domain.Apps.Entities
+namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public abstract class SchemaAggregateCommand : SchemaCommand, IAggregateCommand
+ public abstract class AppCommand : SquidexCommand, IAggregateCommand
{
+ public Guid AppId { get; set; }
+
Guid IAggregateCommand.AggregateId
{
- get { return SchemaId.Id; }
+ get { return AppId; }
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
index 13a60ddda..b54518a66 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
@@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class AssignContributor : AppAggregateCommand
+ public sealed class AssignContributor : AppCommand
{
public string ContributorId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs
index 4239c1e47..0fd8c6c4d 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs
@@ -9,10 +9,15 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class AttachClient : AppAggregateCommand
+ public sealed class AttachClient : AppCommand
{
public string Id { get; set; }
- public string Secret { get; } = RandomHash.New();
+ public string Secret { get; set; }
+
+ public AttachClient()
+ {
+ Secret = RandomHash.New();
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs
index b5af896bf..a323b8b10 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs
@@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class ChangePlan : AppAggregateCommand
+ public sealed class ChangePlan : AppCommand
{
public bool FromCallback { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
index b49d54c59..d4dc2528b 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
@@ -10,16 +10,11 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class CreateApp : SquidexCommand, IAggregateCommand
+ public sealed class CreateApp : AppCommand, IAggregateCommand
{
- public Guid AppId { get; set; }
-
public string Name { get; set; }
- Guid IAggregateCommand.AggregateId
- {
- get { return AppId; }
- }
+ public string Template { get; set; }
public CreateApp()
{
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs
index 5db33b435..199bff83c 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs
@@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class DeletePattern : AppAggregateCommand
+ public sealed class DeletePattern : AppCommand
{
public Guid PatternId { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs
index 6f707811f..a4e27d426 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs
@@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class RemoveContributor : AppAggregateCommand
+ public sealed class RemoveContributor : AppCommand
{
public string ContributorId { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs
index c863e7b85..602c35756 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class RemoveLanguage : AppAggregateCommand
+ public sealed class RemoveLanguage : AppCommand
{
public Language Language { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs
index 623da3058..9361891ba 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs
@@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class RevokeClient : AppAggregateCommand
+ public sealed class RevokeClient : AppCommand
{
public string Id { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
index 856de1f4a..6002bba30 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
@@ -9,7 +9,7 @@ using Squidex.Domain.Apps.Core.Apps;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class UpdateClient : AppAggregateCommand
+ public sealed class UpdateClient : AppCommand
{
public string Id { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
index c0f442a25..52e40e5d0 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class UpdateLanguage : AppAggregateCommand
+ public sealed class UpdateLanguage : AppCommand
{
public Language Language { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs
index 9ae7b510e..415856189 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs
@@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
- public sealed class UpdatePattern : AppAggregateCommand
+ public sealed class UpdatePattern : AppCommand
{
public Guid PatternId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
index 604284090..b46cf9240 100644
--- a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
@@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
if (string.IsNullOrWhiteSpace(command.Id))
{
- error(new ValidationError("Client id must be defined.", nameof(command.Id)));
+ error(new ValidationError("Client id is required.", nameof(command.Id)));
}
else if (clients.ContainsKey(command.Id))
{
@@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
if (string.IsNullOrWhiteSpace(command.Id))
{
- error(new ValidationError("Client id must be defined.", nameof(command.Id)));
+ error(new ValidationError("Client id is required.", nameof(command.Id)));
}
});
}
@@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
if (string.IsNullOrWhiteSpace(command.Id))
{
- error(new ValidationError("Client id must be defined.", nameof(command.Id)));
+ error(new ValidationError("Client id is required.", nameof(command.Id)));
}
if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null)
{
- error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission)));
+ error(new ValidationError("Either name or permission is required.", nameof(command.Name), nameof(command.Permission)));
}
if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue())
diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
new file mode 100644
index 000000000..adea21e4c
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
@@ -0,0 +1,237 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Squidex.Domain.Apps.Core;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Core.Schemas;
+using Squidex.Domain.Apps.Entities.Apps.Commands;
+using Squidex.Domain.Apps.Entities.Contents.Commands;
+using Squidex.Domain.Apps.Entities.Schemas.Commands;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
+using Squidex.Infrastructure.Tasks;
+
+namespace Squidex.Domain.Apps.Entities.Apps.Templates
+{
+ public sealed class CreateBlogCommandMiddleware : ICommandMiddleware
+ {
+ private const string TemplateName = "Blog";
+ private const string SlugScript = @"
+ var data = ctx.data;
+
+ data.slug = { iv: slugify(data.title.iv) };
+
+ replace(data);";
+
+ public Task HandleAsync(CommandContext context, Func next)
+ {
+ if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
+ {
+ var appId = new NamedId(createApp.AppId, createApp.Name);
+
+ return Task.WhenAll(
+ CreatePagesAsync(context.CommandBus, appId),
+ CreatePostsAsync(context.CommandBus, appId),
+ CreateClientAsync(context.CommandBus, appId));
+ }
+
+ return TaskHelper.Done;
+ }
+
+ private static bool IsRightTemplate(CreateApp createApp)
+ {
+ return string.Equals(createApp.Template, TemplateName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static async Task CreateClientAsync(ICommandBus bus, NamedId appId)
+ {
+ await bus.PublishAsync(new AttachClient { Id = "sample-client" });
+ }
+
+ private async Task CreatePostsAsync(ICommandBus bus, NamedId appId)
+ {
+ var postsId = await CreatePostsSchema(bus, appId);
+
+ await bus.PublishAsync(new CreateContent
+ {
+ SchemaId = postsId,
+ Data =
+ new NamedContentData()
+ .AddField("title",
+ new ContentFieldData()
+ .AddValue("iv", "My first post with Squidex"))
+ .AddField("text",
+ new ContentFieldData()
+ .AddValue("iv", "Just created a blog with Squidex. I love it!")),
+ Publish = true,
+ });
+ }
+
+ private async Task CreatePagesAsync(ICommandBus bus, NamedId appId)
+ {
+ var pagesId = await CreatePagesSchema(bus, appId);
+
+ await bus.PublishAsync(new CreateContent
+ {
+ SchemaId = pagesId,
+ Data =
+ new NamedContentData()
+ .AddField("title",
+ new ContentFieldData()
+ .AddValue("iv", "About Me"))
+ .AddField("text",
+ new ContentFieldData()
+ .AddValue("iv", "I love Squidex and SciFi!")),
+ Publish = true
+ });
+ }
+
+ private async Task> CreatePostsSchema(ICommandBus bus, NamedId appId)
+ {
+ var command = new CreateSchema
+ {
+ Name = "posts",
+ Publish = true,
+ Properties = new SchemaProperties
+ {
+ Label = "Posts"
+ },
+ Fields = new List
+ {
+ new CreateSchemaField
+ {
+ Name = "title",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.Input,
+ IsRequired = true,
+ IsListField = true,
+ MaxLength = 100,
+ MinLength = 0,
+ Label = "Title"
+ }
+ },
+ new CreateSchemaField
+ {
+ Name = "slug",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.Slug,
+ IsRequired = false,
+ IsListField = true,
+ MaxLength = 100,
+ MinLength = 0,
+ Label = "Slug (Autogenerated)"
+ },
+ IsDisabled = true
+ },
+ new CreateSchemaField
+ {
+ Name = "text",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.RichText,
+ IsRequired = true,
+ IsListField = false,
+ Label = "Text"
+ }
+ }
+ },
+ AppId = appId
+ };
+
+ await bus.PublishAsync(command);
+
+ var schemaId = new NamedId(command.SchemaId, command.Name);
+
+ await bus.PublishAsync(new ConfigureScripts
+ {
+ SchemaId = schemaId.Id,
+ ScriptCreate = SlugScript,
+ ScriptUpdate = SlugScript
+ });
+
+ return schemaId;
+ }
+
+ private async Task> CreatePagesSchema(ICommandBus bus, NamedId appId)
+ {
+ var command = new CreateSchema
+ {
+ Name = "pages",
+ Properties = new SchemaProperties
+ {
+ Label = "Pages"
+ },
+ Fields = new List
+ {
+ new CreateSchemaField
+ {
+ Name = "title",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.Input,
+ IsRequired = true,
+ IsListField = true,
+ MaxLength = 100,
+ MinLength = 0,
+ Label = "Title"
+ }
+ },
+ new CreateSchemaField
+ {
+ Name = "slug",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.Slug,
+ IsRequired = false,
+ IsListField = true,
+ MaxLength = 100,
+ MinLength = 0,
+ Label = "Slug (Autogenerated)"
+ },
+ IsDisabled = true
+ },
+ new CreateSchemaField
+ {
+ Name = "text",
+ Partitioning = Partitioning.Invariant.Key,
+ Properties = new StringFieldProperties
+ {
+ Editor = StringFieldEditor.RichText,
+ IsRequired = true,
+ IsListField = false,
+ Label = "Text"
+ }
+ }
+ },
+ AppId = appId
+ };
+
+ await bus.PublishAsync(command);
+
+ var schemaId = new NamedId(command.SchemaId, command.Name);
+
+ await bus.PublishAsync(new ConfigureScripts
+ {
+ SchemaId = schemaId.Id,
+ ScriptCreate = SlugScript,
+ ScriptUpdate = SlugScript
+ });
+
+ return schemaId;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
index 360f9134c..7cc351c83 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
@@ -7,6 +7,7 @@
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@@ -73,6 +74,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
return this;
}
+ private void RaiseEvent(AppEvent @event)
+ {
+ if (@event.AppId == null)
+ {
+ @event.AppId = Snapshot.AppId;
+ }
+
+ RaiseEvent(Envelope.Create(@event));
+ }
+
private void VerifyNotCreated()
{
if (!string.IsNullOrWhiteSpace(Snapshot.FileName))
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
similarity index 88%
rename from src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs
rename to src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
index efdb41e00..4898243dd 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
- public abstract class AssetAggregateCommand : AppCommand, IAggregateCommand
+ public abstract class AssetCommand : SquidexCommand, IAggregateCommand
{
public Guid AssetId { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
index f7696e14b..f421d9ed8 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
@@ -6,12 +6,15 @@
// ==========================================================================
using System;
+using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
- public sealed class CreateAsset : AssetAggregateCommand
+ public sealed class CreateAsset : AssetCommand, IAppCommand
{
+ public NamedId AppId { get; set; }
+
public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
index 351333962..4848be209 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
@@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
- public sealed class DeleteAsset : AssetAggregateCommand
+ public sealed class DeleteAsset : AssetCommand
{
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
index 3dc784b88..65cba7f35 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
@@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
- public sealed class RenameAsset : AssetAggregateCommand
+ public sealed class RenameAsset : AssetCommand
{
public string FileName { get; set; }
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
index 8c419ca71..1bb193419 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
- public sealed class UpdateAsset : AssetAggregateCommand
+ public sealed class UpdateAsset : AssetCommand
{
public AssetFile File { get; set; }
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs
new file mode 100644
index 000000000..ffb58c065
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs
@@ -0,0 +1,46 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Microsoft.OData.Edm;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Entities.Assets.Edm
+{
+ public static class EdmAssetModel
+ {
+ public static readonly IEdmModel Edm;
+
+ static EdmAssetModel()
+ {
+ var entityType = new EdmEntityType("Squidex", "Asset");
+
+ entityType.AddStructuralProperty(nameof(IAssetEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int64);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.FileName).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.FileSize).ToCamelCase(), EdmPrimitiveTypeKind.Int64);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.FileVersion).ToCamelCase(), EdmPrimitiveTypeKind.Int64);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.IsImage).ToCamelCase(), EdmPrimitiveTypeKind.Boolean);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.MimeType).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.PixelHeight).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
+ entityType.AddStructuralProperty(nameof(IAssetEntity.PixelWidth).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
+
+ var container = new EdmEntityContainer("Squidex", "Container");
+
+ container.AddEntitySet("AssetSet", entityType);
+
+ var model = new EdmModel();
+
+ model.AddElement(container);
+ model.AddElement(entityType);
+
+ Edm = model;
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
index 756441da2..757959a6a 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
@@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
if (string.IsNullOrWhiteSpace(command.FileName))
{
- error(new ValidationError("Name must be defined.", nameof(command.FileName)));
+ error(new ValidationError("Name is required.", nameof(command.FileName)));
}
if (string.Equals(command.FileName, oldName))
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
index c95179f66..c61c52cc0 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
@@ -5,18 +5,21 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
using Squidex.Domain.Apps.Core.ValidateContent;
+using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetEntity :
IEntity,
- IEntityWithAppRef,
IEntityWithCreatedBy,
IEntityWithLastModifiedBy,
IEntityWithVersion,
IAssetInfo
{
+ NamedId AppId { get; }
+
string MimeType { get; }
long FileVersion { get; }
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
index c2bd7eca1..f748b4936 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
@@ -14,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetRepository
{
- Task> QueryAsync(Guid appId, HashSet mimeTypes = null, HashSet ids = null, string query = null, int take = 10, int skip = 0);
+ Task> QueryAsync(Guid appId, string query = null);
+
+ Task> QueryAsync(Guid appId, HashSet ids);
Task FindAssetAsync(Guid id);
}
diff --git a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
index 8a2f65796..3ed7714a7 100644
--- a/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
+++ b/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
@@ -10,6 +10,7 @@ using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
+using Squidex.Infrastructure;
using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
@@ -18,11 +19,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
{
public class AssetState : DomainObjectState,
IAssetEntity,
- IAssetInfo,
- IUpdateableEntityWithAppRef
+ IAssetInfo
{
[JsonProperty]
- public Guid AppId { get; set; }
+ public NamedId AppId { get; set; }
[JsonProperty]
public string FileName { get; set; }
@@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
SimpleMapper.Map(@event, this);
TotalSize += @event.FileSize;
+
+ AppId = @event.AppId;
}
protected void On(AssetUpdated @event)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
index 9e8de0bd2..e855a9aff 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
@@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// =========================================================================
+using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
@@ -12,5 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public sealed class ChangeContentStatus : ContentCommand
{
public Status Status { get; set; }
+
+ public Instant? DueTime { get; set; }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
index 06b65cfc1..8e15d2a7b 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
@@ -6,15 +6,12 @@
// ==========================================================================
using System;
-using System.Security.Claims;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
- public abstract class ContentCommand : SchemaCommand, IAggregateCommand
+ public abstract class ContentCommand : SquidexCommand, IAggregateCommand
{
- public ClaimsPrincipal User { get; set; }
-
public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
index 1837b977f..2eec65f73 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs
@@ -5,10 +5,22 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System;
+using Squidex.Infrastructure;
+
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
- public sealed class CreateContent : ContentDataCommand
+ public sealed class CreateContent : ContentDataCommand, ISchemaCommand, IAppCommand
{
+ public NamedId AppId { get; set; }
+
+ public NamedId SchemaId { get; set; }
+
public bool Publish { get; set; }
+
+ public CreateContent()
+ {
+ ContentId = Guid.NewGuid();
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
index 6923e5005..7674f1512 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs
@@ -109,9 +109,12 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
GuardContent.CanChangeContentStatus(content.Snapshot.Status, command);
- var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
+ if (!command.DueTime.HasValue)
+ {
+ var operationContext = await CreateContext(command, content, () => "Failed to patch content.");
- await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status);
+ await operationContext.ExecuteScriptAsync(x => x.ScriptChange, command.Status);
+ }
content.ChangeStatus(command);
});
@@ -133,10 +136,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
public async Task HandleAsync(CommandContext context, Func next)
{
- if (!await this.DispatchActionAsync(context.Command, context))
- {
- await next();
- }
+ await this.DispatchActionAsync(context.Command, context);
+ await next();
}
private async Task CreateContext(ContentCommand command, ContentDomainObject content, Func message)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
index d9eb1fa1d..47db34ab1 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
@@ -8,6 +8,7 @@
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.State;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@@ -44,7 +45,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
VerifyCreatedAndNotDeleted();
- RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
+ if (command.DueTime.HasValue)
+ {
+ RaiseEvent(SimpleMapper.Map(command, new ContentStatusScheduled { DueTime = command.DueTime.Value }));
+ }
+ else
+ {
+ RaiseEvent(SimpleMapper.Map(command, new ContentStatusChanged()));
+ }
return this;
}
@@ -79,6 +87,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
return this;
}
+ private void RaiseEvent(SchemaEvent @event)
+ {
+ if (@event.AppId == null)
+ {
+ @event.AppId = Snapshot.AppId;
+ }
+
+ if (@event.SchemaId == null)
+ {
+ @event.SchemaId = Snapshot.SchemaId;
+ }
+
+ RaiseEvent(Envelope.Create(@event));
+ }
+
private void VerifyNotCreated()
{
if (Snapshot.Data != null)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
new file mode 100644
index 000000000..27e1c1895
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs
@@ -0,0 +1,64 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using NodaTime;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Entities.Contents.Commands;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
+
+namespace Squidex.Domain.Apps.Entities.Contents
+{
+ public sealed class ContentEntity : IContentEntity
+ {
+ public Guid Id { get; set; }
+
+ public NamedId AppId { get; set; }
+
+ public NamedId SchemaId { get; set; }
+
+ public long Version { get; set; }
+
+ public Instant Created { get; set; }
+
+ public Instant LastModified { get; set; }
+
+ public Status Status { get; set; }
+
+ public Status? ScheduledTo { get; set; }
+
+ public Instant? ScheduledAt { get; set; }
+
+ public RefToken ScheduledBy { get; set; }
+
+ public RefToken CreatedBy { get; set; }
+
+ public RefToken LastModifiedBy { get; set; }
+
+ public NamedContentData Data { 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
index 63adc8214..6a0237831 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentHistoryEventsCreator.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.History;
+using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@@ -19,16 +20,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
: base(typeNameRegistry)
{
AddEventMessage(
- "created content item.");
+ "created {[Schema]} content item to.");
AddEventMessage(
- "updated content item.");
+ "updated {[Schema]} content item.");
AddEventMessage(
- "deleted content item.");
+ "deleted {[Schema]} content item.");
AddEventMessage(
- "changed status of content item to {[Status]}.");
+ "changed status of {[Schema]} content item to {[Status]}.");
}
protected override Task CreateEventCoreAsync(Envelope @event)
@@ -37,6 +38,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
var result = ForEvent(@event.Payload, channel);
+ if (@event.Payload is SchemaEvent schemaEvent)
+ {
+ result = result.AddParameter("Schema", schemaEvent.SchemaId.Name);
+ }
+
if (@event.Payload is ContentStatusChanged contentStatusChanged)
{
result = result.AddParameter("Status", contentStatusChanged.Status);
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
index d06df5e7c..8660af246 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
@@ -42,7 +42,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
IScriptEngine scriptEngine,
Func message)
{
- var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(command.AppId.Id, command.SchemaId.Id);
+ var a = content.Snapshot.AppId;
+ var s = content.Snapshot.SchemaId;
+
+ if (command is CreateContent createContent)
+ {
+ a = a ?? createContent.AppId;
+ s = s ?? createContent.SchemaId;
+ }
+
+ var (appEntity, schemaEntity) = await appProvider.GetAppWithSchemaAsync(a.Id, s.Id);
var context = new ContentOperationContext
{
@@ -75,17 +84,15 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var errors = new List();
- var appId = command.AppId.Id;
-
var ctx =
new ValidationContext(
(contentIds, schemaId) =>
{
- return QueryContentsAsync(appId, schemaId, contentIds);
+ return QueryContentsAsync(content.Snapshot.AppId.Id, schemaId, contentIds);
},
assetIds =>
{
- return QueryAssetsAsync(appId, assetIds);
+ return QueryAssetsAsync(content.Snapshot.AppId.Id, assetIds);
});
if (partial)
@@ -106,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private async Task> QueryAssetsAsync(Guid appId, IEnumerable assetIds)
{
- return await assetRepository.QueryAsync(appId, null, new HashSet(assetIds), null, int.MaxValue, 0);
+ return await assetRepository.QueryAsync(appId, new HashSet(assetIds));
}
private async Task> QueryContentsAsync(Guid appId, Guid schemaId, IEnumerable contentIds)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
index ae01ee691..ebd65e858 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs
@@ -12,7 +12,6 @@ using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.OData;
using Microsoft.OData.UriParser;
-using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps;
@@ -122,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var content in contents)
{
var contentData = scriptEngine.Transform(new ScriptContext { User = user, Data = content.Data, ContentId = content.Id }, scriptText);
- var contentResult = SimpleMapper.Map(content, new Content());
+ var contentResult = SimpleMapper.Map(content, new ContentEntity());
contentResult.Data = contentData;
@@ -199,23 +198,5 @@ namespace Squidex.Domain.Apps.Entities.Contents
return status;
}
-
- private sealed class Content : IContentEntity
- {
- public Guid Id { get; set; }
- public Guid AppId { get; set; }
-
- public long Version { get; set; }
-
- public Instant Created { get; set; }
- public Instant LastModified { get; set; }
-
- public RefToken CreatedBy { get; set; }
- public RefToken LastModifiedBy { get; set; }
-
- public NamedContentData Data { get; set; }
-
- public Status Status { get; set; }
- }
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs
new file mode 100644
index 000000000..23d3c05a9
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs
@@ -0,0 +1,57 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Threading.Tasks;
+using NodaTime;
+using Squidex.Domain.Apps.Entities.Contents.Commands;
+using Squidex.Domain.Apps.Entities.Contents.Repositories;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
+using Squidex.Infrastructure.Timers;
+
+namespace Squidex.Domain.Apps.Entities.Contents
+{
+ public sealed class ContentScheduler : IRunnable
+ {
+ private readonly CompletionTimer timer;
+ private readonly IContentRepository contentRepository;
+ private readonly ICommandBus commandBus;
+ private readonly IClock clock;
+
+ public ContentScheduler(
+ IContentRepository contentRepository,
+ ICommandBus commandBus,
+ IClock clock)
+ {
+ Guard.NotNull(contentRepository, nameof(contentRepository));
+ Guard.NotNull(commandBus, nameof(commandBus));
+ Guard.NotNull(clock, nameof(clock));
+
+ this.contentRepository = contentRepository;
+ this.commandBus = commandBus;
+ this.clock = clock;
+
+ timer = new CompletionTimer(5000, x => PublishAsync());
+ }
+
+ public void Run()
+ {
+ }
+
+ private Task PublishAsync()
+ {
+ var now = clock.GetCurrentInstant();
+
+ return contentRepository.QueryScheduledWithoutDataAsync(now, content =>
+ {
+ var command = new ChangeContentStatus { ContentId = content.Id, Status = content.ScheduledTo.Value, Actor = content.ScheduledBy };
+
+ return commandBus.PublishAsync(command);
+ });
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
index 5a73c8c59..ea8ffbe18 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/Edm/EdmModelBuilder.cs
@@ -44,8 +44,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm
{
var model = new EdmModel();
- var container = new EdmEntityContainer("Squidex", "Container");
-
var schemaType = schema.BuildEdmType(partitionResolver, x =>
{
model.AddElement(x);
@@ -54,19 +52,21 @@ namespace Squidex.Domain.Apps.Entities.Contents.Edm
});
var entityType = new EdmEntityType("Squidex", schema.Name);
- entityType.AddStructuralProperty("data", new EdmComplexTypeReference(schemaType, false));
- entityType.AddStructuralProperty("version", EdmPrimitiveTypeKind.Int32);
- entityType.AddStructuralProperty("created", EdmPrimitiveTypeKind.DateTimeOffset);
- entityType.AddStructuralProperty("createdBy", EdmPrimitiveTypeKind.String);
- entityType.AddStructuralProperty("lastModified", EdmPrimitiveTypeKind.DateTimeOffset);
- entityType.AddStructuralProperty("lastModifiedBy", EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IContentEntity.Created).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
+ entityType.AddStructuralProperty(nameof(IContentEntity.CreatedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IContentEntity.LastModified).ToCamelCase(), EdmPrimitiveTypeKind.DateTimeOffset);
+ entityType.AddStructuralProperty(nameof(IContentEntity.LastModifiedBy).ToCamelCase(), EdmPrimitiveTypeKind.String);
+ entityType.AddStructuralProperty(nameof(IContentEntity.Version).ToCamelCase(), EdmPrimitiveTypeKind.Int32);
+ entityType.AddStructuralProperty(nameof(IContentEntity.Data).ToCamelCase(), new EdmComplexTypeReference(schemaType, false));
+
+ var container = new EdmEntityContainer("Squidex", "Container");
+
+ container.AddEntitySet("ContentSet", entityType);
model.AddElement(container);
model.AddElement(schemaType);
model.AddElement(entityType);
- container.AddEntitySet("ContentSet", entityType);
-
return model;
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
index d970ca7e6..eb3d9f315 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
@@ -13,6 +13,7 @@ using Microsoft.Extensions.Caching.Memory;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
@@ -20,6 +21,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IContentQueryService contentQuery;
+ private readonly ICommandBus commandBus;
private readonly IGraphQLUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IAppProvider appProvider;
@@ -27,17 +29,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public CachingGraphQLService(IMemoryCache cache,
IAppProvider appProvider,
IAssetRepository assetRepository,
+ ICommandBus commandBus,
IContentQueryService contentQuery,
IGraphQLUrlGenerator urlGenerator)
: base(cache)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(assetRepository, nameof(assetRepository));
- Guard.NotNull(contentQuery, nameof(urlGenerator));
+ Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(contentQuery, nameof(contentQuery));
+ Guard.NotNull(urlGenerator, nameof(urlGenerator));
this.appProvider = appProvider;
this.assetRepository = assetRepository;
+ this.commandBus = commandBus;
this.contentQuery = contentQuery;
this.urlGenerator = urlGenerator;
}
@@ -53,9 +58,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
var modelContext = await GetModelAsync(app);
- var queryContext = new GraphQLQueryContext(app, assetRepository, contentQuery, user, urlGenerator);
- return await modelContext.ExecuteAsync(queryContext, query);
+ var ctx = new GraphQLExecutionContext(app, assetRepository, commandBus, contentQuery, user, urlGenerator);
+
+ return await modelContext.ExecuteAsync(ctx, query);
}
private async Task GetModelAsync(IAppEntity app)
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
similarity index 84%
rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs
rename to src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
index b050ab970..f73f3ab6d 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLQueryContext.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
@@ -13,17 +13,22 @@ using Newtonsoft.Json.Linq;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
+using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
- public sealed class GraphQLQueryContext : QueryContext
+ public sealed class GraphQLExecutionContext : QueryContext
{
+ public ICommandBus CommandBus { get; }
+
public IGraphQLUrlGenerator UrlGenerator { get; }
- public GraphQLQueryContext(IAppEntity app, IAssetRepository assetRepository, IContentQueryService contentQuery, ClaimsPrincipal user,
+ public GraphQLExecutionContext(IAppEntity app, IAssetRepository assetRepository, ICommandBus commandBus, IContentQueryService contentQuery, ClaimsPrincipal user,
IGraphQLUrlGenerator urlGenerator)
: base(app, assetRepository, contentQuery, user)
{
+ CommandBus = commandBus;
+
UrlGenerator = urlGenerator;
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
index e2915016a..03ceeec54 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
@@ -24,15 +24,17 @@ using GraphQLSchema = GraphQL.Types.Schema;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
- public sealed class GraphQLModel : IGraphQLContext
+ public sealed class GraphQLModel : IGraphModel
{
private readonly Dictionary> fieldInfos;
- private readonly Dictionary schemaTypes = new Dictionary();
+ private readonly Dictionary inputFieldInfos;
+ private readonly Dictionary contentTypes = new Dictionary();
+ private readonly Dictionary contentDataTypes = new Dictionary();
private readonly Dictionary schemas;
private readonly PartitionResolver partitionResolver;
private readonly IAppEntity app;
- private readonly IGraphType assetType;
private readonly IGraphType assetListType;
+ private readonly IComplexGraphType assetType;
private readonly GraphQLSchema graphQLSchema;
public bool CanGenerateAssetSourceUrl { get; }
@@ -48,35 +50,71 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
assetType = new AssetGraphType(this);
assetListType = new ListGraphType(new NonNullGraphType(assetType));
- fieldInfos = new Dictionary>
+ inputFieldInfos = new Dictionary
{
{
typeof(StringField),
- field => ResolveDefault("String")
+ AllTypes.String
},
{
typeof(BooleanField),
- field => ResolveDefault("Boolean")
+ AllTypes.Boolean
},
{
typeof(NumberField),
- field => ResolveDefault("Float")
+ AllTypes.Boolean
},
{
typeof(DateTimeField),
- field => ResolveDefault("Date")
+ AllTypes.Date
},
{
- typeof(JsonField),
- field => ResolveDefault("Json")
+ typeof(GeolocationField),
+ AllTypes.GeolocationInput
},
{
typeof(TagsField),
- field => ResolveDefault("String")
+ AllTypes.ListOfNonNullString
+ },
+ {
+ typeof(AssetsField),
+ AllTypes.ListOfNonNullGuid
+ },
+ {
+ typeof(ReferencesField),
+ AllTypes.ListOfNonNullGuid
+ }
+ };
+
+ fieldInfos = new Dictionary>
+ {
+ {
+ typeof(StringField),
+ field => ResolveDefault(AllTypes.NoopString)
+ },
+ {
+ typeof(BooleanField),
+ field => ResolveDefault(AllTypes.NoopBoolean)
+ },
+ {
+ typeof(NumberField),
+ field => ResolveDefault(AllTypes.NoopFloat)
+ },
+ {
+ typeof(DateTimeField),
+ field => ResolveDefault(AllTypes.NoopDate)
+ },
+ {
+ typeof(JsonField),
+ field => ResolveDefault(AllTypes.NoopJson)
},
{
typeof(GeolocationField),
- field => ResolveDefault("Geolocation")
+ field => ResolveDefault(AllTypes.NoopGeolocation)
+ },
+ {
+ typeof(TagsField),
+ field => ResolveDefault(AllTypes.NoopTags)
},
{
typeof(AssetsField),
@@ -90,24 +128,32 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
this.schemas = schemas.ToDictionary(x => x.Id);
- graphQLSchema = new GraphQLSchema { Query = new ContentQueryGraphType(this, this.schemas.Values) };
+ var m = new AppMutationsGraphType(this, this.schemas.Values);
+ var q = new AppQueriesGraphType(this, this.schemas.Values);
+
+ graphQLSchema = new GraphQLSchema { Query = q, Mutation = m };
- foreach (var schemaType in schemaTypes.Values)
+ foreach (var kvp in contentDataTypes)
{
- schemaType.Initialize();
+ kvp.Value.Initialize(this, kvp.Key);
+ }
+
+ foreach (var kvp in contentTypes)
+ {
+ kvp.Value.Initialize(this, kvp.Key, contentDataTypes[kvp.Key]);
}
}
- private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(string name)
+ private static (IGraphType ResolveType, IFieldResolver Resolver) ResolveDefault(IGraphType type)
{
- return (new NoopGraphType(name), new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName)));
+ return (type, new FuncFieldResolver(c => c.Source.GetOrDefault(c.FieldName)));
}
public IFieldResolver ResolveAssetUrl()
{
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetUrl(app, c.Source);
});
@@ -119,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetSourceUrl(app, c.Source);
});
@@ -131,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source);
});
@@ -143,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source);
});
@@ -155,7 +201,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedAssetsAsync(contentIds);
@@ -167,27 +213,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private ValueTuple ResolveReferences(Field field)
{
var schemaId = ((ReferencesField)field).Properties.SchemaId;
- var schemaType = GetSchemaType(schemaId);
- if (schemaType == null)
+ var contentType = GetContentType(schemaId);
+
+ if (contentType == null)
{
return (null, null);
}
var resolver = new FuncFieldResolver(c =>
{
- var context = (GraphQLQueryContext)c.UserContext;
+ var context = (GraphQLExecutionContext)c.UserContext;
var contentIds = c.Source.GetOrDefault(c.FieldName);
return context.GetReferencedContentsAsync(schemaId, contentIds);
});
- var schemaFieldType = new ListGraphType(new NonNullGraphType(GetSchemaType(schemaId)));
+ var schemaFieldType = new ListGraphType(new NonNullGraphType(contentType));
return (schemaFieldType, resolver);
}
- public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLQueryContext context, GraphQLQuery query)
+ public async Task<(object Data, object[] Errors)> ExecuteAsync(GraphQLExecutionContext context, GraphQLQuery query)
{
Guard.NotNull(context, nameof(context));
@@ -208,7 +255,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return partitionResolver(key);
}
- public IGraphType GetAssetType()
+ public IComplexGraphType GetAssetType()
{
return assetType;
}
@@ -218,11 +265,33 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
return fieldInfos[field.GetType()](field);
}
- public IGraphType GetSchemaType(Guid schemaId)
+ public IComplexGraphType GetContentDataType(Guid schemaId)
{
var schema = schemas.GetOrDefault(schemaId);
- return schema != null ? schemaTypes.GetOrAdd(schemaId, k => new ContentGraphType(schema, this)) : null;
+ if (schema == null)
+ {
+ return null;
+ }
+
+ return schema != null ? contentDataTypes.GetOrAdd(schema, s => new ContentDataGraphType()) : null;
+ }
+
+ public IComplexGraphType GetContentType(Guid schemaId)
+ {
+ var schema = schemas.GetOrDefault(schemaId);
+
+ if (schema == null)
+ {
+ return null;
+ }
+
+ return contentTypes.GetOrAdd(schema, s => new ContentGraphType());
+ }
+
+ public IGraphType GetInputGraphType(Field field)
+ {
+ return inputFieldInfos.GetOrAddDefault(field.GetType());
}
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
similarity index 80%
rename from src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs
rename to src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
index 45e00873a..29834fa71 100644
--- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLContext.cs
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphModel.cs
@@ -14,15 +14,17 @@ using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
- public interface IGraphQLContext
+ public interface IGraphModel
{
bool CanGenerateAssetSourceUrl { get; }
IFieldPartitioning ResolvePartition(Partitioning key);
- IGraphType GetAssetType();
+ IComplexGraphType GetAssetType();
- IGraphType GetSchemaType(Guid schemaId);
+ IComplexGraphType GetContentType(Guid schemaId);
+
+ IComplexGraphType GetContentDataType(Guid schemaId);
IFieldResolver ResolveAssetUrl();
@@ -32,6 +34,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IFieldResolver ResolveContentUrl(ISchemaEntity schema);
+ IGraphType GetInputGraphType(Field field);
+
(IGraphType ResolveType, IFieldResolver Resolver) GetGraphType(Field field);
}
}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
new file mode 100644
index 000000000..dac8e7e07
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs
@@ -0,0 +1,67 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using GraphQL.Types;
+
+namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
+{
+ public static class AllTypes
+ {
+ public static readonly Type None = typeof(NoopGraphType);
+
+ public static readonly IGraphType Int = new IntGraphType();
+
+ public static readonly IGraphType Guid = new GuidGraphType();
+
+ public static readonly IGraphType Date = new DateGraphType();
+
+ public static readonly IGraphType Float = new FloatGraphType();
+
+ public static readonly IGraphType String = new StringGraphType();
+
+ public static readonly IGraphType Boolean = new BooleanGraphType();
+
+ public static readonly IGraphType NonNullInt = new NonNullGraphType(new IntGraphType());
+
+ public static readonly IGraphType NonNullGuid = new NonNullGraphType(new GuidGraphType());
+
+ public static readonly IGraphType NonNullDate = new NonNullGraphType(new DateGraphType());
+
+ public static readonly IGraphType NonNullFloat = new NonNullGraphType(new FloatGraphType());
+
+ public static readonly IGraphType NonNullString = new NonNullGraphType(new StringGraphType());
+
+ public static readonly IGraphType NonNullBoolean = new NonNullGraphType(new BooleanGraphType());
+
+ public static readonly IGraphType ListOfNonNullGuid = new ListGraphType(new NonNullGraphType(new GuidGraphType()));
+
+ public static readonly IGraphType ListOfNonNullString = new ListGraphType(new NonNullGraphType(new StringGraphType()));
+
+ public static readonly IGraphType NoopInt = new NoopGraphType("Int");
+
+ public static readonly IGraphType NoopGuid = new NoopGraphType("Guid");
+
+ public static readonly IGraphType NoopDate = new NoopGraphType("Date");
+
+ public static readonly IGraphType NoopJson = new NoopGraphType("Json");
+
+ public static readonly IGraphType NoopTags = new NoopGraphType("Tags");
+
+ public static readonly IGraphType NoopFloat = new NoopGraphType("Float");
+
+ public static readonly IGraphType NoopString = new NoopGraphType("String");
+
+ public static readonly IGraphType NoopBoolean = new NoopGraphType("Boolean");
+
+ public static readonly IGraphType NoopGeolocation = new NoopGraphType("Geolocation");
+
+ public static readonly IGraphType CommandVersion = new CommandVersionGraphType();
+
+ public static readonly IGraphType GeolocationInput = new GeolocationInputGraphType();
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
new file mode 100644
index 000000000..58ec5d018
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AppMutationsGraphType.cs
@@ -0,0 +1,344 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using GraphQL;
+using GraphQL.Resolvers;
+using GraphQL.Types;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.Contents;
+using Squidex.Domain.Apps.Entities.Contents.Commands;
+using Squidex.Domain.Apps.Entities.Schemas;
+using Squidex.Infrastructure;
+using Squidex.Infrastructure.Commands;
+
+namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
+{
+ public sealed class AppMutationsGraphType : ObjectGraphType
+ {
+ public AppMutationsGraphType(IGraphModel model, IEnumerable schemas)
+ {
+ foreach (var schema in schemas)
+ {
+ var schemaId = schema.NamedId();
+ var schemaType = schema.TypeName();
+ var schemaName = schema.DisplayName();
+
+ var contentType = model.GetContentType(schema.Id);
+ var contentDataType = model.GetContentDataType(schema.Id);
+
+ var resultType = new ContentDataChangedResultGraphType(schemaType, schemaName, contentDataType);
+
+ var inputType = new ContentDataGraphInputType(model, schema);
+
+ AddContentCreate(schemaId, schemaType, schemaName, inputType, contentDataType, contentType);
+ AddContentUpdate(schemaType, schemaName, inputType, resultType);
+ AddContentPatch(schemaType, schemaName, inputType, resultType);
+ AddContentPublish(schemaType, schemaName);
+ AddContentUnpublish(schemaType, schemaName);
+ AddContentArchive(schemaType, schemaName);
+ AddContentRestore(schemaType, schemaName);
+ AddContentDelete(schemaType, schemaName);
+ }
+
+ Description = "The app mutations.";
+ }
+
+ private void AddContentCreate(NamedId schemaId, string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType contentDataType, IComplexGraphType contentType)
+ {
+ AddField(new FieldType
+ {
+ Name = $"create{schemaType}Content",
+ Arguments = new QueryArguments
+ {
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "data",
+ Description = $"The data for the {schemaName} content.",
+ DefaultValue = null,
+ ResolvedType = new NonNullGraphType(inputType),
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "publish",
+ Description = "Set to true to autopublish content.",
+ DefaultValue = false,
+ ResolvedType = AllTypes.Boolean
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "expectedVersion",
+ Description = "The expected version",
+ DefaultValue = EtagVersion.Any,
+ ResolvedType = AllTypes.Int
+ }
+ },
+ ResolvedType = new NonNullGraphType(contentType),
+ Resolver = ResolveAsync(async (c, publish) =>
+ {
+ var argPublish = c.GetArgument("publish");
+
+ var contentData = GetContentData(c);
+
+ var command = new CreateContent { SchemaId = schemaId, Data = contentData, Publish = argPublish };
+ var commandContext = await publish(command);
+
+ var result = commandContext.Result>();
+ var response = ContentEntity.Create(command, result);
+
+ return (IContentEntity)ContentEntity.Create(command, result);
+ }),
+ Description = $"Creates an {schemaName} content."
+ });
+ }
+
+ private void AddContentUpdate(string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType)
+ {
+ AddField(new FieldType
+ {
+ Name = $"update{schemaType}Content",
+ Arguments = new QueryArguments
+ {
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "id",
+ Description = $"The id of the {schemaName} content (GUID)",
+ DefaultValue = string.Empty,
+ ResolvedType = AllTypes.NonNullGuid
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "data",
+ Description = $"The data for the {schemaName} content.",
+ DefaultValue = null,
+ ResolvedType = new NonNullGraphType(inputType),
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "expectedVersion",
+ Description = "The expected version",
+ DefaultValue = EtagVersion.Any,
+ ResolvedType = AllTypes.Int
+ }
+ },
+ ResolvedType = new NonNullGraphType(resultType),
+ Resolver = ResolveAsync(async (c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+ var contentData = GetContentData(c);
+
+ var command = new UpdateContent { ContentId = contentId, Data = contentData };
+ var commandContext = await publish(command);
+
+ var result = commandContext.Result();
+
+ return result;
+ }),
+ Description = $"Update an {schemaName} content by id."
+ });
+ }
+
+ private void AddContentPatch(string schemaType, string schemaName, ContentDataGraphInputType inputType, IComplexGraphType resultType)
+ {
+ AddField(new FieldType
+ {
+ Name = $"patch{schemaType}Content",
+ Arguments = new QueryArguments
+ {
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "id",
+ Description = $"The id of the {schemaName} content (GUID)",
+ DefaultValue = string.Empty,
+ ResolvedType = AllTypes.NonNullGuid
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "data",
+ Description = $"The data for the {schemaName} content.",
+ DefaultValue = null,
+ ResolvedType = new NonNullGraphType(inputType),
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "expectedVersion",
+ Description = "The expected version",
+ DefaultValue = EtagVersion.Any,
+ ResolvedType = AllTypes.Int
+ }
+ },
+ ResolvedType = new NonNullGraphType(resultType),
+ Resolver = ResolveAsync(async (c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+ var contentData = GetContentData(c);
+
+ var command = new PatchContent { ContentId = contentId, Data = contentData };
+ var commandContext = await publish(command);
+
+ var result = commandContext.Result();
+
+ return result;
+ }),
+ Description = $"Patch a {schemaName} content."
+ });
+ }
+
+ private void AddContentPublish(string schemaType, string schemaName)
+ {
+ AddField(new FieldType
+ {
+ Name = $"publish{schemaType}Content",
+ Arguments = CreateIdArguments(schemaName),
+ ResolvedType = AllTypes.CommandVersion,
+ Resolver = ResolveAsync((c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+
+ var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Published };
+
+ return publish(command);
+ }),
+ Description = $"Publish a {schemaName} content."
+ });
+ }
+
+ private void AddContentUnpublish(string schemaType, string schemaName)
+ {
+ AddField(new FieldType
+ {
+ Name = $"unpublish{schemaType}Content",
+ Arguments = CreateIdArguments(schemaName),
+ ResolvedType = AllTypes.CommandVersion,
+ Resolver = ResolveAsync((c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+
+ var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
+
+ return publish(command);
+ }),
+ Description = $"Unpublish a {schemaName} content."
+ });
+ }
+
+ private void AddContentArchive(string schemaType, string schemaName)
+ {
+ AddField(new FieldType
+ {
+ Name = $"archive{schemaType}Content",
+ Arguments = CreateIdArguments(schemaName),
+ ResolvedType = AllTypes.CommandVersion,
+ Resolver = ResolveAsync((c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+
+ var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Archived };
+
+ return publish(command);
+ }),
+ Description = $"Archive a {schemaName} content."
+ });
+ }
+
+ private void AddContentRestore(string schemaType, string schemaName)
+ {
+ AddField(new FieldType
+ {
+ Name = $"restore{schemaType}Content",
+ Arguments = CreateIdArguments(schemaName),
+ ResolvedType = AllTypes.CommandVersion,
+ Resolver = ResolveAsync((c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+
+ var command = new ChangeContentStatus { ContentId = contentId, Status = Status.Draft };
+
+ return publish(command);
+ }),
+ Description = $"Restore a {schemaName} content."
+ });
+ }
+
+ private void AddContentDelete(string schemaType, string schemaName)
+ {
+ AddField(new FieldType
+ {
+ Name = $"delete{schemaType}Content",
+ Arguments = CreateIdArguments(schemaName),
+ ResolvedType = AllTypes.CommandVersion,
+ Resolver = ResolveAsync((c, publish) =>
+ {
+ var contentId = c.GetArgument("id");
+
+ var command = new DeleteContent { ContentId = contentId };
+
+ return publish(command);
+ }),
+ Description = $"Delete an {schemaName} content."
+ });
+ }
+
+ private static QueryArguments CreateIdArguments(string schemaName)
+ {
+ return new QueryArguments
+ {
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "id",
+ Description = $"The id of the {schemaName} content (GUID)",
+ DefaultValue = string.Empty,
+ ResolvedType = AllTypes.NonNullGuid
+ },
+ new QueryArgument(AllTypes.None)
+ {
+ Name = "expectedVersion",
+ Description = "The expected version",
+ DefaultValue = EtagVersion.Any,
+ ResolvedType = AllTypes.Int
+ }
+ };
+ }
+
+ private static IFieldResolver ResolveAsync(Func>, Task> action)
+ {
+ return new FuncFieldResolver>(async c =>
+ {
+ var e = (GraphQLExecutionContext)c.UserContext;
+
+ try
+ {
+ return await action(c, command =>
+ {
+ command.ExpectedVersion = c.GetArgument("expectedVersion", EtagVersion.Any);
+
+ return e.CommandBus.PublishAsync(command);
+ });
+ }
+ catch (ValidationException ex)
+ {
+ c.Errors.Add(new ExecutionError(ex.Message));
+
+ throw;
+ }
+ catch (DomainException ex)
+ {
+ c.Errors.Add(new ExecutionError(ex.Message));
+
+ throw;
+ }
+ });
+ }
+
+ private static NamedContentData GetContentData(ResolveFieldContext c)
+ {
+ return JObject.FromObject(c.GetArgument