Browse Source

Merge branch 'master' into json-eventstore

# Conflicts:
#	src/Squidex.Infrastructure.GetEventStore/EventSourcing/ProjectionHelper.cs
#	src/Squidex/Config/Domain/SerializationServices.cs
#	src/Squidex/Config/Domain/WriteServices.cs
#	tools/Migrate_01/Rebuilder.cs
pull/218/head
Sebastian Stehle 8 years ago
parent
commit
525fa41cc4
  1. 78
      CHANGELOG.md
  2. 6
      README.md
  3. 9
      Squidex.sln.DotSettings
  4. 11
      build.sh
  5. 0
      libs/keep
  6. BIN
      libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg
  7. 1
      libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512
  8. 23
      libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec
  9. BIN
      libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg
  10. 1
      libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512
  11. 25
      libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec
  12. 4
      src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
  13. 20
      src/Squidex.Domain.Apps.Core.Model/Freezable.cs
  14. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
  15. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
  16. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
  17. 25
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
  18. 31
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
  19. 8
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  20. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs
  21. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  22. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  23. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs
  24. 28
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/AssetChangedTrigger.cs
  25. 29
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs
  26. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
  27. 181
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  28. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  29. 76
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  30. 46
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  31. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  32. 33
      src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs
  33. 76
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  34. 46
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  35. 1
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldEditor.cs
  36. 106
      src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  37. 31
      src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs
  38. 2
      src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj
  39. 157
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AlgoliaActionHandler.cs
  40. 73
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/AzureQueueActionHandler.cs
  41. 89
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/FastlyActionHandler.cs
  42. 99
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/SlackActionHandler.cs
  43. 45
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs
  44. 39
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/ClientPool.cs
  45. 25
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/HttpClientPool.cs
  46. 170
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  47. 9
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  48. 31
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/AssetChangedTriggerHandler.cs
  49. 5
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs
  50. 2
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  51. 2
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataProperty.cs
  52. 5
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  53. 8
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  54. 62
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  55. 68
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  56. 13
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs
  57. 86
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  58. 24
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentEntity.cs
  59. 80
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  60. 6
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  61. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs
  62. 87
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs
  63. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventEntity.cs
  64. 17
      src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs
  65. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository_SnapshotStore.cs
  66. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository_SnapshotStore.cs
  67. 4
      src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj
  68. 4
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  69. 12
      src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs
  70. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs
  71. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs
  72. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs
  73. 8
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs
  74. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AssignContributor.cs
  75. 9
      src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs
  76. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs
  77. 9
      src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs
  78. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs
  79. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs
  80. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs
  81. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs
  82. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs
  83. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs
  84. 2
      src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs
  85. 8
      src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs
  86. 237
      src/Squidex.Domain.Apps.Entities/Apps/Templates/CreateBlogCommandMiddleware.cs
  87. 11
      src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  88. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs
  89. 5
      src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  90. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
  91. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs
  92. 2
      src/Squidex.Domain.Apps.Entities/Assets/Commands/UpdateAsset.cs
  93. 46
      src/Squidex.Domain.Apps.Entities/Assets/Edm/EdmAssetModel.cs
  94. 2
      src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  95. 5
      src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
  96. 4
      src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  97. 8
      src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  98. 3
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ChangeContentStatus.cs
  99. 5
      src/Squidex.Domain.Apps.Entities/Contents/Commands/ContentCommand.cs
  100. 14
      src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs

78
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.

6
README.md

@ -1,6 +1,6 @@
![Squidex Logo](https://raw.githubusercontent.com/Squidex/squidex/master/media/logo-wide.png "Squidex") ![Squidex Logo](https://raw.githubusercontent.com/Squidex/squidex/master/media/logo-wide.png "Squidex")
# 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. 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 ## 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 ## Prerequisites
@ -22,7 +22,7 @@ Current Version 1.0-beta3. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadma
## Contributors ## Contributors
### Core Team ### Core Team and Founders
* [Qaisar Ahmad](http://www.qaisarahmad.com/) Interaction Designer, Pakistan * [Qaisar Ahmad](http://www.qaisarahmad.com/) Interaction Designer, Pakistan
* [Sebastian Stehle](https://github.com/SebastianStehle) Software Engineer, Germany (currently Sweden) * [Sebastian Stehle](https://github.com/SebastianStehle) Software Engineer, Germany (currently Sweden)

9
Squidex.sln.DotSettings

@ -36,12 +36,15 @@
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Header/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Header"&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String> <s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Header/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Header"&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Namespaces/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Namespaces"&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String> <s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Namespaces/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Namespaces"&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Typescript/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Typescript"&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;/Profile&gt;</s:String> <s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Typescript/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Typescript"&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;/Profile&gt;</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">==========================================================================&#xD; <s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">==========================================================================&#xD;
$FILENAME$&#xD;
Squidex Headless CMS&#xD; Squidex Headless CMS&#xD;
==========================================================================&#xD; ==========================================================================&#xD;
Copyright (c) Squidex Group&#xD; Copyright (c) Squidex UG (haftungsbeschraenkt)&#xD;
All rights reserved.&#xD; All rights reserved. Licensed under the MIT license.&#xD;
==========================================================================&#xD; ==========================================================================&#xD;
</s:String> </s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary> </wpf:ResourceDictionary>

11
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

0
libs/keep

BIN
libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg

Binary file not shown.

1
libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.2.0.0-beta1-fix.nupkg.sha512

@ -1 +0,0 @@
5W20j9jiNog4dHUEt+cCnePb8z6jFEMnkwO4XilajM7FCnen3KTnN/G8PAUGuQieSlTI9MRe0sRYcafLJl900w==

23
libs/microsoft.orleans.orleanscodegenerator.build/2.0.0-beta1-fix/microsoft.orleans.orleanscodegenerator.build.nuspec

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>Microsoft.Orleans.OrleansCodeGenerator.Build</id>
<version>2.0.0-beta1-fix</version>
<title>Microsoft Orleans Build-time Code Generator</title>
<authors>Microsoft</authors>
<owners>Microsoft</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<developmentDependency>true</developmentDependency>
<licenseUrl>https://github.com/dotnet/Orleans#license</licenseUrl>
<projectUrl>https://github.com/dotnet/Orleans</projectUrl>
<iconUrl>https://raw.githubusercontent.com/dotnet/orleans/gh-pages/assets/logo_128.png</iconUrl>
<description>Microsoft Orleans build-time code generator to install in all grain interface &amp; implementation projects.</description>
<copyright>© Microsoft Corporation. All rights reserved.</copyright>
<tags>Orleans Cloud-Computing Actor-Model Actors Distributed-Systems C# .NET</tags>
<repository type="git" url="https://github.com/dotnet/Orleans" />
<dependencies>
<group targetFramework=".NETFramework4.6.1" />
<group targetFramework=".NETCoreApp2.0" />
</dependencies>
</metadata>
</package>

BIN
libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg

Binary file not shown.

1
libs/orleansdashboard/2.0.0-beta3/orleansdashboard.2.0.0-beta3.nupkg.sha512

@ -1 +0,0 @@
mBHlGWl+bNTPP463JBEB/dftmdZKQRD8X72F7lsTFqYWddW5Ytp1gbzChCxW0d/Pt71KLF6XrVmyecbFlNdFBA==

25
libs/orleansdashboard/2.0.0-beta3/orleansdashboard.nuspec

@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
<metadata>
<id>OrleansDashboard</id>
<version>2.0.0-beta3</version>
<authors>OrleansContrib</authors>
<owners>OrleansContrib</owners>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<licenseUrl>https://opensource.org/licenses/MIT</licenseUrl>
<projectUrl>https://github.com/OrleansContrib/OrleansDashboard</projectUrl>
<iconUrl>http://dotnet.github.io/orleans/assets/logo.png</iconUrl>
<description>An admin dashboard for Microsoft Orleans</description>
<copyright>Copyright © 2017</copyright>
<tags>orleans dashboard metrics monitor</tags>
<repository url="https://github.com/OrleansContrib/OrleansDashboard" />
<dependencies>
<group targetFramework=".NETStandard2.0">
<dependency id="Microsoft.AspNetCore" version="2.0.1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Orleans.Core" version="2.0.0-beta1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Orleans.OrleansCodeGenerator.Build" version="2.0.0-beta1" exclude="Build,Analyzers" />
<dependency id="Microsoft.Orleans.OrleansRuntime" version="2.0.0-beta1" exclude="Build,Analyzers" />
</group>
</dependencies>
</metadata>
</package>

4
src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<Freezable/>
</Weavers>

20
src/Squidex.Infrastructure/Freezable.cs → src/Squidex.Domain.Apps.Core.Model/Freezable.cs

@ -6,24 +6,30 @@
// ========================================================================== // ==========================================================================
using System; 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"); throw new InvalidOperationException("Object is frozen");
} }
} }
public void Freeze() public virtual void Freeze()
{ {
IsFrozen = true; isFrozen = true;
} }
} }
} }

26
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<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

24
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<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

24
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<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

25
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<T>(IRuleActionVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

31
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs

@ -13,36 +13,9 @@ namespace Squidex.Domain.Apps.Core.Rules.Actions
[TypeName(nameof(WebhookAction))] [TypeName(nameof(WebhookAction))]
public sealed class WebhookAction : RuleAction public sealed class WebhookAction : RuleAction
{ {
private Uri url; public Uri Url { get; set; }
private string sharedSecret;
public Uri Url public string SharedSecret { get; set; }
{
get
{
return url;
}
set
{
ThrowIfFrozen();
url = value;
}
}
public string SharedSecret
{
get
{
return sharedSecret;
}
set
{
ThrowIfFrozen();
sharedSecret = value;
}
}
public override T Accept<T>(IRuleActionVisitor<T> visitor) public override T Accept<T>(IRuleActionVisitor<T> visitor)
{ {

8
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs

@ -11,6 +11,14 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
public interface IRuleActionVisitor<out T> public interface IRuleActionVisitor<out T>
{ {
T Visit(AlgoliaAction action);
T Visit(AzureQueueAction action);
T Visit(FastlyAction action);
T Visit(SlackAction action);
T Visit(WebhookAction action); T Visit(WebhookAction action);
} }
} }

2
src/Squidex.Domain.Apps.Core.Model/Rules/IRuleTriggerVisitor.cs

@ -11,6 +11,8 @@ namespace Squidex.Domain.Apps.Core.Rules
{ {
public interface IRuleTriggerVisitor<out T> public interface IRuleTriggerVisitor<out T>
{ {
T Visit(AssetChangedTrigger trigger);
T Visit(ContentChangedTrigger trigger); T Visit(ContentChangedTrigger trigger);
} }
} }

2
src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public abstract class RuleAction : Freezable public abstract class RuleAction : Freezable

2
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 AppId { get; set; }
public Guid AggregateId { get; set; }
public string EventName { get; set; } public string EventName { get; set; }
public string ActionName { get; set; } public string ActionName { get; set; }

2
src/Squidex.Domain.Apps.Core.Model/Rules/RuleTrigger.cs

@ -5,8 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules namespace Squidex.Domain.Apps.Core.Rules
{ {
public abstract class RuleTrigger : Freezable public abstract class RuleTrigger : Freezable

28
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<T>(IRuleTriggerVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

29
src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTrigger.cs

@ -13,25 +13,26 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers
[TypeName(nameof(ContentChangedTrigger))] [TypeName(nameof(ContentChangedTrigger))]
public sealed class ContentChangedTrigger : RuleTrigger public sealed class ContentChangedTrigger : RuleTrigger
{ {
private ImmutableList<ContentChangedTriggerSchema> schemas; public ImmutableList<ContentChangedTriggerSchema> Schemas { get; set; }
public ImmutableList<ContentChangedTriggerSchema> Schemas public bool HandleAll { get; set; }
{
get
{
return schemas;
}
set
{
ThrowIfFrozen();
schemas = value;
}
}
public override T Accept<T>(IRuleTriggerVisitor<T> visitor) public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{ {
return visitor.Visit(this); return visitor.Visit(this);
} }
public override void Freeze()
{
base.Freeze();
if (Schemas != null)
{
foreach (var schema in Schemas)
{
schema.Freeze();
}
}
}
} }
} }

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

@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Core.Rules.Triggers namespace Squidex.Domain.Apps.Core.Rules.Triggers
{ {
public sealed class ContentChangedTriggerSchema public sealed class ContentChangedTriggerSchema : Freezable
{ {
public Guid SchemaId { get; set; } public Guid SchemaId { get; set; }

181
src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -13,186 +13,29 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(AssetsField))] [TypeName(nameof(AssetsField))]
public sealed class AssetsFieldProperties : FieldProperties public sealed class AssetsFieldProperties : FieldProperties
{ {
private bool mustBeImage; public bool MustBeImage { get; set; }
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<string> allowedExtensions;
public bool MustBeImage public int? MinItems { get; set; }
{
get
{
return mustBeImage;
}
set
{
ThrowIfFrozen();
mustBeImage = value; public int? MaxItems { get; set; }
}
}
public int? MinItems public int? MinWidth { get; set; }
{
get
{
return minItems;
}
set
{
ThrowIfFrozen();
minItems = value; public int? MaxWidth { get; set; }
}
}
public int? MaxItems public int? MinHeight { get; set; }
{
get
{
return maxItems;
}
set
{
ThrowIfFrozen();
maxItems = value; public int? MaxHeight { get; set; }
}
}
public int? MinWidth public int? MinSize { get; set; }
{
get
{
return minWidth;
}
set
{
ThrowIfFrozen();
minWidth = value; public int? MaxSize { get; set; }
}
}
public int? MaxWidth public int? AspectWidth { get; set; }
{
get
{
return maxWidth;
}
set
{
ThrowIfFrozen();
maxWidth = value; public int? AspectHeight { get; set; }
}
}
public int? MinHeight public ImmutableList<string> AllowedExtensions { get; set; }
{
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<string> AllowedExtensions
{
get
{
return allowedExtensions;
}
set
{
ThrowIfFrozen();
allowedExtensions = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

31
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs

@ -12,36 +12,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(BooleanField))] [TypeName(nameof(BooleanField))]
public sealed class BooleanFieldProperties : FieldProperties public sealed class BooleanFieldProperties : FieldProperties
{ {
private BooleanFieldEditor editor; public bool? DefaultValue { get; set; }
private bool? defaultValue;
public bool? DefaultValue public BooleanFieldEditor Editor { get; set; }
{
get
{
return defaultValue;
}
set
{
ThrowIfFrozen();
defaultValue = value;
}
}
public BooleanFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

76
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs

@ -13,81 +13,15 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(DateTimeField))] [TypeName(nameof(DateTimeField))]
public sealed class DateTimeFieldProperties : FieldProperties public sealed class DateTimeFieldProperties : FieldProperties
{ {
private DateTimeFieldEditor editor; public Instant? MaxValue { get; set; }
private DateTimeCalculatedDefaultValue? calculatedDefaultValue;
private Instant? maxValue;
private Instant? minValue;
private Instant? defaultValue;
public Instant? MaxValue public Instant? MinValue { get; set; }
{
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();
calculatedDefaultValue = value; public Instant? DefaultValue { get; set; }
}
}
public DateTimeFieldEditor Editor public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; }
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value; public DateTimeFieldEditor Editor { get; set; }
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

46
src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs

@ -9,51 +9,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class FieldProperties : NamedElementPropertiesBase public abstract class FieldProperties : NamedElementPropertiesBase
{ {
private bool isRequired; public bool IsRequired { get; set; }
private bool isListField;
private string placeholder;
public bool IsRequired public bool IsListField { get; set; }
{
get
{
return isRequired;
}
set
{
ThrowIfFrozen();
isRequired = value; public string Placeholder { get; set; }
}
}
public bool IsListField
{
get
{
return isListField;
}
set
{
ThrowIfFrozen();
isListField = value;
}
}
public string Placeholder
{
get
{
return placeholder;
}
set
{
ThrowIfFrozen();
placeholder = value;
}
}
public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor); public abstract T Accept<T>(IFieldPropertiesVisitor<T> visitor);

16
src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs

@ -12,21 +12,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(GeolocationField))] [TypeName(nameof(GeolocationField))]
public sealed class GeolocationFieldProperties : FieldProperties public sealed class GeolocationFieldProperties : FieldProperties
{ {
private GeolocationFieldEditor editor; public GeolocationFieldEditor Editor { get; set; }
public GeolocationFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

33
src/Squidex.Domain.Apps.Core.Model/Schemas/NamedElementPropertiesBase.cs

@ -5,41 +5,12 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
public abstract class NamedElementPropertiesBase : Freezable public abstract class NamedElementPropertiesBase : Freezable
{ {
private string label; public string Label { get; set; }
private string hints;
public string Label
{
get
{
return label;
}
set
{
ThrowIfFrozen();
label = value;
}
}
public string Hints
{
get
{
return hints;
}
set
{
ThrowIfFrozen();
hints = value; public string Hints { get; set; }
}
}
} }
} }

76
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs

@ -13,81 +13,15 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(NumberField))] [TypeName(nameof(NumberField))]
public sealed class NumberFieldProperties : FieldProperties public sealed class NumberFieldProperties : FieldProperties
{ {
private double? maxValue; public ImmutableList<double> AllowedValues { get; set; }
private double? minValue;
private double? defaultValue;
private ImmutableList<double> allowedValues;
private NumberFieldEditor editor;
public double? MaxValue public double? MaxValue { get; set; }
{
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<double> AllowedValues
{
get
{
return allowedValues;
}
set
{
ThrowIfFrozen();
allowedValues = value; public double? MinValue { get; set; }
}
}
public NumberFieldEditor Editor public double? DefaultValue { get; set; }
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value; public NumberFieldEditor Editor { get; set; }
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

46
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs

@ -13,51 +13,11 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(ReferencesField))] [TypeName(nameof(ReferencesField))]
public sealed class ReferencesFieldProperties : FieldProperties public sealed class ReferencesFieldProperties : FieldProperties
{ {
private int? minItems; public int? MinItems { get; set; }
private int? maxItems;
private Guid schemaId;
public int? MinItems public int? MaxItems { get; set; }
{
get
{
return minItems;
}
set
{
ThrowIfFrozen();
minItems = value;
}
}
public int? MaxItems
{
get
{
return maxItems;
}
set
{
ThrowIfFrozen();
maxItems = value; public Guid SchemaId { get; set; }
}
}
public Guid SchemaId
{
get
{
return schemaId;
}
set
{
ThrowIfFrozen();
schemaId = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

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

@ -14,6 +14,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
Dropdown, Dropdown,
Radio, Radio,
RichText, RichText,
Slug,
TextArea TextArea
} }
} }

106
src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -13,111 +13,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(StringField))] [TypeName(nameof(StringField))]
public sealed class StringFieldProperties : FieldProperties public sealed class StringFieldProperties : FieldProperties
{ {
private int? minLength; public ImmutableList<string> AllowedValues { get; set; }
private int? maxLength;
private string pattern;
private string patternMessage;
private string defaultValue;
private ImmutableList<string> allowedValues;
private StringFieldEditor editor;
public int? MinLength public int? MinLength { get; set; }
{
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 string Pattern public int? MaxLength { get; set; }
{
get
{
return pattern;
}
set
{
ThrowIfFrozen();
pattern = value;
}
}
public string PatternMessage public string DefaultValue { get; set; }
{
get
{
return patternMessage;
}
set
{
ThrowIfFrozen();
patternMessage = value; public string Pattern { get; set; }
}
}
public ImmutableList<string> AllowedValues public string PatternMessage { get; set; }
{
get
{
return allowedValues;
}
set
{
ThrowIfFrozen();
allowedValues = value; public StringFieldEditor Editor { get; set; }
}
}
public StringFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

31
src/Squidex.Domain.Apps.Core.Model/Schemas/TagsFieldProperties.cs

@ -12,36 +12,9 @@ namespace Squidex.Domain.Apps.Core.Schemas
[TypeName(nameof(TagsField))] [TypeName(nameof(TagsField))]
public sealed class TagsFieldProperties : FieldProperties public sealed class TagsFieldProperties : FieldProperties
{ {
private int? minItems; public int? MinItems { get; set; }
private int? maxItems;
public int? MinItems public int? MaxItems { get; set; }
{
get
{
return minItems;
}
set
{
ThrowIfFrozen();
minItems = value;
}
}
public int? MaxItems
{
get
{
return maxItems;
}
set
{
ThrowIfFrozen();
maxItems = value;
}
}
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)
{ {

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

@ -8,6 +8,8 @@
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Fody" Version="2.3.24" />
<PackageReference Include="Freezable.Fody" Version="1.8.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup> </ItemGroup>

157
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<AlgoliaAction>
{
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<AppEvent> @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<string>();
var apiKey = job["ApiKey"].Value<string>();
var indexName = job["IndexName"].Value<string>();
var index = clients.GetClient((appId, apiKey, indexName));
var operation = operationToken.Value<string>();
var content = job["Content"].Value<JObject>();
var contentId = job["ContentId"].Value<string>();
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);
}
}
}
}

73
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<AzureQueueAction>
{
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<AppEvent> @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<string>();
var queueName = job["QueueName"].Value<string>();
var queue = clients.GetClient((queueConnectionString, queueName));
var messageBody = job["MessageBody"].ToString(Formatting.Indented);
await queue.AddMessageAsync(new CloudQueueMessage(messageBody));
return ("Completed", null);
}
}
}

89
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<FastlyAction>
{
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @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<string>());
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<string, JToken> job, string key)
{
var serviceId = job["FastlyServiceID"].Value<string>();
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<string>());
return request;
}
}
}

99
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<SlackAction>
{
private readonly RuleEventFormatter formatter;
public SlackActionHandler(RuleEventFormatter formatter)
{
Guard.NotNull(formatter, nameof(formatter));
this.formatter = formatter;
}
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @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<AppEvent> @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<string, JToken> job, string requestBody)
{
var requestUrl = job["RequestUrl"].Value<string>();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{
Content = new StringContent(requestBody, Encoding.UTF8, "application/json")
};
return request;
}
}
}

45
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<WebhookAction> public sealed class WebhookActionHandler : RuleActionHandler<WebhookAction>
{ {
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(2); private readonly RuleEventFormatter formatter;
private readonly JsonSerializer serializer; public WebhookActionHandler(RuleEventFormatter formatter)
public WebhookActionHandler(JsonSerializer serializer)
{ {
Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(formatter, nameof(formatter));
this.serializer = serializer; this.formatter = formatter;
} }
protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @event, string eventName, WebhookAction action) protected override (string Description, RuleJobData Data) CreateJob(Envelope<AppEvent> @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 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 var ruleData = new RuleJobData
{ {
["RequestUrl"] = action.Url, ["RequestUrl"] = action.Url,
@ -51,38 +49,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
return (ruleDescription, ruleData); return (ruleDescription, ruleData);
} }
private JObject CreatePayload(Envelope<AppEvent> @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) public override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(RuleJobData job)
{ {
var requestBody = job["RequestBody"].ToString(Formatting.Indented); var requestBody = job["RequestBody"].ToString(Formatting.Indented);
var request = BuildRequest(job, requestBody); var requestMsg = BuildRequest(job, requestBody);
HttpResponseMessage response = null; HttpResponseMessage response = null;
try try
{ {
using (var client = new HttpClient { Timeout = Timeout }) response = await HttpClientPool.GetHttpClient().SendAsync(requestMsg);
{
response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync(); var responseString = await response.Content.ReadAsStringAsync();
var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString, TimeSpan.Zero, false); var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false);
return (requestDump, null); return (requestDump, null);
}
} }
catch (Exception ex) 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); return (requestDump, ex);
} }
@ -97,15 +84,15 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody) private static HttpRequestMessage BuildRequest(Dictionary<string, JToken> job, string requestBody)
{ {
var requestUrl = job["RequestUrl"].ToString(); var requestUrl = job["RequestUrl"].Value<string>();
var requestSignature = job["RequestSignature"].ToString(); var requestSig = job["RequestSignature"].Value<string>();
var request = new HttpRequestMessage(HttpMethod.Post, requestUrl) var request = new HttpRequestMessage(HttpMethod.Post, requestUrl)
{ {
Content = new StringContent(requestBody, Encoding.UTF8, "application/json") 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"); request.Headers.Add("User-Agent", "Squidex Webhook");
return request; return request;

39
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<TKey, TClient>
{
private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30);
private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly Func<TKey, TClient> factory;
public ClientPool(Func<TKey, TClient> factory)
{
this.factory = factory;
}
public TClient GetClient(TKey key)
{
if (!memoryCache.TryGetValue<TClient>(key, out var client))
{
client = factory(key);
memoryCache.Set(key, client, TTL);
}
return client;
}
}
}

25
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<string, HttpClient> Pool = new ClientPool<string, HttpClient>(key =>
{
return new HttpClient { Timeout = TimeSpan.FromSeconds(2) };
});
public static HttpClient GetHttpClient()
{
return Pool.GetClient(string.Empty);
}
}
}

170
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<AppEvent> @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<AppEvent> @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<AppEvent> @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;
});
}
}
}

9
src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -87,16 +87,17 @@ namespace Squidex.Domain.Apps.Core.HandleRules
@event.Headers.Timestamp() : @event.Headers.Timestamp() :
now; now;
var eventGuid = var aggregateId =
@event.Headers.Contains(CommonHeaders.EventId) ? @event.Headers.Contains(CommonHeaders.AggregateId) ?
@event.Headers.EventId() : @event.Headers.AggregateId() :
Guid.NewGuid(); Guid.NewGuid();
var job = new RuleJob var job = new RuleJob
{ {
JobId = eventGuid, JobId = Guid.NewGuid(),
ActionName = actionName, ActionName = actionName,
ActionData = actionData.Data, ActionData = actionData.Data,
AggregateId = aggregateId,
AppId = appEvent.AppId.Id, AppId = appEvent.AppId.Id,
Created = now, Created = now,
EventName = eventName, EventName = eventName,

31
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<AssetChangedTrigger>
{
protected override bool Triggers(Envelope<AppEvent> @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);
}
}
}

5
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<AppEvent> @event, ContentChangedTrigger trigger) protected override bool Triggers(Envelope<AppEvent> @event, ContentChangedTrigger trigger)
{ {
if (trigger.HandleAll)
{
return true;
}
if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent) if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent)
{ {
foreach (var schema in trigger.Schemas) foreach (var schema in trigger.Schemas)

2
src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
{ {
EnsurePropertiesInitialized(); 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<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties() public override IEnumerable<KeyValuePair<string, PropertyDescriptor>> GetOwnProperties()

2
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()) 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(); var obj = value.AsObject();

5
src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs

@ -115,11 +115,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
} }
catch (ParserException ex) 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) 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("ctx", contextInstance);
engine.SetValue("slugify", new Func<string, string>(x => x.Slugify()));
return engine; return engine;
} }

8
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -13,15 +13,17 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.1" />
<PackageReference Include="Jint" Version="2.11.58" /> <PackageReference Include="Jint" Version="2.11.58" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
<PackageReference Include="NJsonSchema" Version="9.10.15" /> <PackageReference Include="NJsonSchema" Version="9.10.19" />
<PackageReference Include="NodaTime" Version="2.2.3" /> <PackageReference Include="NodaTime" Version="2.2.3" />
<PackageReference Include="RefactoringEssentials" Version="5.4.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.Collections.Immutable" Version="1.4.0" /> <PackageReference Include="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="WindowsAzure.Storage" Version="8.7.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

62
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -8,24 +8,74 @@
using System; using System;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; 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; using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed class MongoAssetEntity : IVersionedEntity<Guid> public sealed class MongoAssetEntity :
MongoEntity,
IAssetEntity,
IUpdateableEntityWithVersion,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy
{ {
[BsonId] [BsonRequired]
[BsonElement]
public Guid AppIdId { get; set; }
[BsonRequired]
[BsonElement]
public NamedId<Guid> AppId { get; set; }
[BsonRequired]
[BsonElement]
public string MimeType { get; set; }
[BsonRequired]
[BsonElement] [BsonElement]
[BsonRepresentation(BsonType.String)] public string FileName { get; set; }
public Guid Id { get; set; }
[BsonRequired]
[BsonElement] [BsonElement]
public long FileSize { get; set; }
[BsonRequired] [BsonRequired]
public AssetState State { get; set; } [BsonElement]
public long FileVersion { get; set; }
[BsonRequired]
[BsonElement] [BsonElement]
public bool IsImage { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement]
public long Version { get; set; } 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; }
}
} }
} }

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

@ -9,10 +9,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets; 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.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
@ -34,44 +35,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
return collection.Indexes.CreateOneAsync( return collection.Indexes.CreateOneAsync(
Index Index
.Ascending(x => x.State.AppId) .Ascending(x => x.AppId)
.Ascending(x => x.State.IsDeleted) .Ascending(x => x.IsDeleted)
.Ascending(x => x.State.FileName) .Ascending(x => x.FileName)
.Ascending(x => x.State.MimeType) .Descending(x => x.LastModified));
.Descending(x => x.State.LastModified));
} }
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null, int take = 10, int skip = 0) public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null)
{ {
var filters = new List<FilterDefinition<MongoAssetEntity>> try
{ {
Filter.Eq(x => x.State.AppId, appId), var odataQuery = EdmAssetModel.Edm.ParseQuery(query);
Filter.Eq(x => x.State.IsDeleted, false)
};
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<IAssetEntity>(contentItems.Result, contentCount.Result);
}
catch (NotSupportedException)
{ {
filters.Add(Filter.In(x => x.Id, ids)); throw new ValidationException("This odata operation is not supported.");
} }
catch (NotImplementedException)
if (mimeTypes != null && mimeTypes.Count > 0)
{ {
filters.Add(Filter.In(x => x.State.MimeType, mimeTypes)); throw new ValidationException("This odata operation is not supported.");
} }
catch (MongoQueryException ex)
if (!string.IsNullOrWhiteSpace(query))
{ {
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<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> 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 assetItems = find.ToListAsync();
var assetCount = Collection.Find(filter).CountAsync(); var assetCount = find.CountAsync();
await Task.WhenAll(assetItems, assetCount); await Task.WhenAll(assetItems, assetCount);
return ResultList.Create<IAssetEntity>(assetItems.Result.Select(x => x.State), assetCount.Result); return ResultList.Create(assetItems.Result.OfType<IAssetEntity>().ToList(), assetCount.Result);
} }
public async Task<IAssetEntity> FindAssetAsync(Guid id) public async Task<IAssetEntity> FindAssetAsync(Guid id)
@ -80,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id) await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
return assetEntity?.State; return assetEntity;
} }
} }
} }

13
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository_SnapshotStore.cs

@ -10,7 +10,7 @@ using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
@ -25,15 +25,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null) if (existing != null)
{ {
return (existing.State, existing.Version); return (SimpleMapper.Map(existing, new AssetState()), existing.Version);
} }
return (null, EtagVersion.NotFound); 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);
} }
} }
} }

86
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<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.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<MongoAssetEntity, MongoAssetEntity> AssetSort(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
var sort = query.BuildSort<MongoAssetEntity>(PropertyCalculator);
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetTake(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
return cursor.Take(query, 200, 20);
}
public static IFindFluent<MongoAssetEntity, MongoAssetEntity> AssetSkip(this IFindFluent<MongoAssetEntity, MongoAssetEntity> cursor, ODataUriParser query)
{
return cursor.Skip(query);
}
public static FilterDefinition<MongoAssetEntity> BuildQuery(ODataUriParser query, Guid appId)
{
var filters = new List<FilterDefinition<MongoAssetEntity>>
{
Filter.Eq(x => x.AppIdId, appId),
Filter.Eq(x => x.IsDeleted, false)
};
var filter = query.BuildFilter<MongoAssetEntity>(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();
}
}
}
}

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

@ -35,12 +35,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonRequired] [BsonRequired]
[BsonElement("ai")] [BsonElement("ai")]
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public Guid AppId { get; set; } public Guid AppIdId { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("si")] [BsonElement("si")]
[BsonRepresentation(BsonType.String)] [BsonRepresentation(BsonType.String)]
public Guid SchemaId { get; set; } public Guid SchemaIdId { get; set; }
[BsonRequired] [BsonRequired]
[BsonElement("rf")] [BsonElement("rf")]
@ -62,6 +62,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonJson] [BsonJson]
public IdContentData DataByIds { get; set; } public IdContentData DataByIds { get; set; }
[BsonRequired]
[BsonElement("ai2")]
public NamedId<Guid> AppId { get; set; }
[BsonRequired]
[BsonElement("si2")]
public NamedId<Guid> 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] [BsonRequired]
[BsonElement("ct")] [BsonElement("ct")]
public Instant Created { get; set; } public Instant Created { get; set; }

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

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents;
@ -49,6 +50,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> collection) protected override async Task SetupCollectionAsync(IMongoCollection<MongoContentEntity> 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( await archiveCollection.Indexes.CreateOneAsync(
Index Index
.Ascending(x => x.Id) .Ascending(x => x.Id)
@ -56,30 +63,45 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateOneAsync( await collection.Indexes.CreateOneAsync(
Index Index
.Ascending(x => x.SchemaId) .Text(x => x.DataText)
.Ascending(x => x.SchemaIdId)
.Ascending(x => x.Status) .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)); .Ascending(x => x.IsDeleted));
await collection.Indexes.CreateOneAsync( await collection.Indexes.CreateOneAsync(
Index Index
.Ascending(x => x.SchemaIdId)
.Ascending(x => x.Id) .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)); await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
{ {
FilterDefinition<MongoContentEntity> filter;
try 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<IContentEntity>(contentItems.Result, contentCount.Result);
} }
catch (NotSupportedException) catch (NotSupportedException)
{ {
@ -89,23 +111,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{ {
throw new ValidationException("This odata operation is not supported."); throw new ValidationException("This odata operation is not supported.");
} }
catch (MongoQueryException ex)
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)
{ {
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<IContentEntity>(contentItems.Result, contentCount.Result);
} }
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> ids) public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, HashSet<Guid> 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 contentItems = find.ToListAsync();
var contentCount = find.CountAsync(); var contentCount = find.CountAsync();
@ -120,13 +141,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result); return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result);
} }
public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> contentIds) public async Task<IReadOnlyList<Guid>> QueryNotFoundAsync(Guid appId, Guid schemaId, IList<Guid> ids)
{ {
var contentEntities = 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(); .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<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version) public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, long version)
@ -143,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id) public async Task<IContentEntity> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id)
{ {
var contentEntity = 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(); .FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef); contentEntity?.ParseData(schema.SchemaDef);
@ -151,6 +172,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contentEntity; return contentEntity;
} }
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
{
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted == false)
.ForEachAsync(c =>
{
callback(c);
});
}
public override async Task ClearAsync() public override async Task ClearAsync()
{ {
await Database.DropCollectionAsync("States_Contents_Archive"); await Database.DropCollectionAsync("States_Contents_Archive");

6
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.Domain.Apps.Events.Contents;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents 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)), Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.ContentId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId)); Update.AddToSet(x => x.ReferencedIdsDeleted, @event.ContentId));
} }
Task IEventConsumer.ClearAsync()
{
return TaskHelper.Done;
}
} }
} }

10
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs

@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
if (contentEntity != null) if (contentEntity != null)
{ {
var schema = await GetSchemaAsync(contentEntity.AppId, contentEntity.SchemaId); var schema = await GetSchemaAsync(contentEntity.AppIdId, contentEntity.SchemaIdId);
contentEntity?.ParseData(schema.SchemaDef); 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) public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
{ {
if (value.SchemaId == Guid.Empty) if (value.SchemaId.Id == Guid.Empty)
{ {
return; 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); 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 var document = SimpleMapper.Map(value, new MongoContentEntity
{ {
AppIdId = value.AppId.Id,
SchemaIdId = value.SchemaId.Id,
IsDeleted = value.IsDeleted, IsDeleted = value.IsDeleted,
DocumentId = key.ToString(), DocumentId = key.ToString(),
DataText = idData?.ToFullText(), DataText = idData?.ToFullText(),
@ -92,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId) private async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid schemaId)
{ {
var schema = await appProvider.GetSchemaAsync(appId, schemaId); var schema = await appProvider.GetSchemaAsync(appId, schemaId, true);
if (schema == null) if (schema == null)
{ {

87
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Visitors/FindExtensions.cs

@ -7,10 +7,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OData.UriParser; using Microsoft.OData.UriParser;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb.OData;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
@ -18,64 +23,80 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{ {
private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter; private static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
public static IFindFluent<MongoContentEntity, MongoContentEntity> Sort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query, Schema schema) private static readonly Dictionary<string, string> PropertyMap =
typeof(MongoContentEntity).GetProperties()
.ToDictionary(x => x.Name, x => x.GetCustomAttribute<BsonElementAttribute>()?.ElementName ?? x.Name, StringComparer.OrdinalIgnoreCase);
static FindExtensions()
{ {
return cursor.Sort(SortBuilder.BuildSort(query, schema)); PropertyMap["Data"] = "do";
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> Take(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static PropertyCalculator CreatePropertyCalculator(Schema schema)
{ {
var top = query.ParseTop(); return propertyNames =>
if (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, 200));
}
else
{ {
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<MongoContentEntity, MongoContentEntity> Skip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSort(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query, PropertyCalculator propertyCalculator)
{ {
var skip = query.ParseSkip(); var sort = query.BuildSort<MongoContentEntity>(propertyCalculator);
if (skip.HasValue) return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
{
cursor = cursor.Skip((int)skip.Value);
}
else
{
cursor = cursor.Skip(null);
}
return cursor;
} }
public static IFindFluent<MongoContentEntity, MongoContentEntity> Find(this IMongoCollection<MongoContentEntity> cursor, ODataUriParser query, Guid schemaId, Schema schema, Status[] status) public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentTake(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query)
{ {
var filter = BuildQuery(query, schemaId, schema, status); return cursor.Take(query, 200, 20);
}
return cursor.Find(filter); public static IFindFluent<MongoContentEntity, MongoContentEntity> ContentSkip(this IFindFluent<MongoContentEntity, MongoContentEntity> cursor, ODataUriParser query)
{
return cursor.Skip(query);
} }
public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, Guid schemaId, Schema schema, Status[] status) public static FilterDefinition<MongoContentEntity> BuildQuery(ODataUriParser query, Guid schemaId, Status[] status, PropertyCalculator propertyCalculator)
{ {
var filters = new List<FilterDefinition<MongoContentEntity>> var filters = new List<FilterDefinition<MongoContentEntity>>
{ {
Filter.Eq(x => x.SchemaId, schemaId), Filter.Eq(x => x.SchemaIdId, schemaId),
Filter.In(x => x.Status, status), Filter.In(x => x.Status, status),
Filter.Eq(x => x.IsDeleted, false) Filter.Eq(x => x.IsDeleted, false)
}; };
var filter = FilterBuilder.Build(query, schema); var filter = query.BuildFilter<MongoContentEntity>(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) if (filters.Count == 1)

4
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, public sealed class MongoHistoryEventEntity : MongoEntity,
IEntity, IEntity,
IEntityWithAppRef,
IUpdateableEntity, IUpdateableEntity,
IUpdateableEntityWithVersion, IUpdateableEntityWithVersion,
IUpdateableEntityWithCreatedBy, IUpdateableEntityWithCreatedBy
IUpdateableEntityWithAppRef
{ {
[BsonElement] [BsonElement]
[BsonRequired] [BsonRequired]

17
src/Squidex.Domain.Apps.Entities.MongoDb/History/MongoHistoryEventRepository.cs

@ -66,9 +66,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.History
public async Task<IReadOnlyList<IHistoryEventEntity>> QueryByChannelAsync(Guid appId, string channelPrefix, int count) public async Task<IReadOnlyList<IHistoryEventEntity>> QueryByChannelAsync(Guid appId, string channelPrefix, int count)
{ {
var historyEventEntities = List<MongoHistoryEventEntity> historyEventEntities;
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
.ToListAsync(); 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(); return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
} }

2
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 return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u
.Set(x => x.State, value) .Set(x => x.State, value)
.Set(x => x.AppId, value.AppId) .Set(x => x.AppId, value.AppId.Id)
.Set(x => x.IsDeleted, value.IsDeleted)); .Set(x => x.IsDeleted, value.IsDeleted));
} }
} }

2
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 return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u
.Set(x => x.State, value) .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.Name, value.Name)
.Set(x => x.IsDeleted, value.IsDeleted)); .Set(x => x.IsDeleted, value.IsDeleted));
} }

4
src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj

@ -15,9 +15,9 @@
<ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" /> <ProjectReference Include="..\Squidex.Domain.Apps.Entities\Squidex.Domain.Apps.Entities.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" /> <PackageReference Include="Microsoft.OData.Core" Version="7.4.0" />
<PackageReference Include="MongoDB.Driver" Version="2.5.0" /> <PackageReference Include="MongoDB.Driver" Version="2.5.0" />
<PackageReference Include="RefactoringEssentials" Version="5.4.0" /> <PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" /> <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" /> <PackageReference Include="System.ValueTuple" Version="4.4.0" />
</ItemGroup> </ItemGroup>

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

@ -88,11 +88,11 @@ namespace Squidex.Domain.Apps.Entities
return (await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId)).Snapshot; return (await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId)).Snapshot;
} }
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id) public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{ {
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id); var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id);
if (!IsFound(schema)) if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId)
{ {
return null; return null;
} }

12
src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs

@ -45,9 +45,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.appPlansBillingManager = appPlansBillingManager; this.appPlansBillingManager = appPlansBillingManager;
} }
protected Task On(CreateApp command, CommandContext context) protected async Task On(CreateApp command, CommandContext context)
{ {
return handler.CreateSyncedAsync<AppDomainObject>(context, async a => var app = await handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
{ {
await GuardApp.CanCreate(command, appProvider); await GuardApp.CanCreate(command, appProvider);
@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
} }
else 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) if (result is PlanChangedResult)
{ {
@ -193,10 +193,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task HandleAsync(CommandContext context, Func<Task> next) public async Task HandleAsync(CommandContext context, Func<Task> next)
{ {
if (!await this.DispatchActionAsync(context.Command, context)) await this.DispatchActionAsync(context.Command, context);
{ await next();
await next();
}
} }
} }
} }

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

@ -133,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event) protected override Task<HistoryEventToStore> CreateEventCoreAsync(Envelope<IEvent> @event)
{ {
return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null); return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);
} }
private static string ClientName(AppClientRenamed @event) private static string ClientName(AppClientRenamed @event)

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/AddLanguage.cs

@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class AddLanguage : AppAggregateCommand public sealed class AddLanguage : AppCommand
{ {
public Language Language { get; set; } public Language Language { get; set; }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs

@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class AddPattern : AppAggregateCommand public sealed class AddPattern : AppCommand
{ {
public Guid PatternId { get; set; } public Guid PatternId { get; set; }

8
src/Squidex.Domain.Apps.Entities/SchemaAggregateCommand.cs → src/Squidex.Domain.Apps.Entities/Apps/Commands/AppCommand.cs

@ -8,13 +8,15 @@
using System; using System;
using Squidex.Infrastructure.Commands; 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 Guid IAggregateCommand.AggregateId
{ {
get { return SchemaId.Id; } get { return AppId; }
} }
} }
} }

2
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 namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class AssignContributor : AppAggregateCommand public sealed class AssignContributor : AppCommand
{ {
public string ContributorId { get; set; } public string ContributorId { get; set; }

9
src/Squidex.Domain.Apps.Entities/Apps/Commands/AttachClient.cs

@ -9,10 +9,15 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class AttachClient : AppAggregateCommand public sealed class AttachClient : AppCommand
{ {
public string Id { get; set; } public string Id { get; set; }
public string Secret { get; } = RandomHash.New(); public string Secret { get; set; }
public AttachClient()
{
Secret = RandomHash.New();
}
} }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/ChangePlan.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class ChangePlan : AppAggregateCommand public sealed class ChangePlan : AppCommand
{ {
public bool FromCallback { get; set; } public bool FromCallback { get; set; }

9
src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs

@ -10,16 +10,11 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.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; } public string Name { get; set; }
Guid IAggregateCommand.AggregateId public string Template { get; set; }
{
get { return AppId; }
}
public CreateApp() public CreateApp()
{ {

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs

@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class DeletePattern : AppAggregateCommand public sealed class DeletePattern : AppCommand
{ {
public Guid PatternId { get; set; } public Guid PatternId { get; set; }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveContributor.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class RemoveContributor : AppAggregateCommand public sealed class RemoveContributor : AppCommand
{ {
public string ContributorId { get; set; } public string ContributorId { get; set; }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/RemoveLanguage.cs

@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class RemoveLanguage : AppAggregateCommand public sealed class RemoveLanguage : AppCommand
{ {
public Language Language { get; set; } public Language Language { get; set; }
} }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/RevokeClient.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class RevokeClient : AppAggregateCommand public sealed class RevokeClient : AppCommand
{ {
public string Id { get; set; } public string Id { get; set; }
} }

2
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 namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdateClient : AppAggregateCommand public sealed class UpdateClient : AppCommand
{ {
public string Id { get; set; } public string Id { get; set; }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateLanguage.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdateLanguage : AppAggregateCommand public sealed class UpdateLanguage : AppCommand
{ {
public Language Language { get; set; } public Language Language { get; set; }

2
src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs

@ -9,7 +9,7 @@ using System;
namespace Squidex.Domain.Apps.Entities.Apps.Commands namespace Squidex.Domain.Apps.Entities.Apps.Commands
{ {
public sealed class UpdatePattern : AppAggregateCommand public sealed class UpdatePattern : AppCommand
{ {
public Guid PatternId { get; set; } public Guid PatternId { get; set; }

8
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)) 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)) else if (clients.ContainsKey(command.Id))
{ {
@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{ {
if (string.IsNullOrWhiteSpace(command.Id)) 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)) 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) 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()) if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue())

237
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<Task> next)
{
if (context.IsCompleted && context.Command is CreateApp createApp && IsRightTemplate(createApp))
{
var appId = new NamedId<Guid>(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<Guid> appId)
{
await bus.PublishAsync(new AttachClient { Id = "sample-client" });
}
private async Task CreatePostsAsync(ICommandBus bus, NamedId<Guid> 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<Guid> 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<NamedId<Guid>> CreatePostsSchema(ICommandBus bus, NamedId<Guid> appId)
{
var command = new CreateSchema
{
Name = "posts",
Publish = true,
Properties = new SchemaProperties
{
Label = "Posts"
},
Fields = new List<CreateSchemaField>
{
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<Guid>(command.SchemaId, command.Name);
await bus.PublishAsync(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = SlugScript,
ScriptUpdate = SlugScript
});
return schemaId;
}
private async Task<NamedId<Guid>> CreatePagesSchema(ICommandBus bus, NamedId<Guid> appId)
{
var command = new CreateSchema
{
Name = "pages",
Properties = new SchemaProperties
{
Label = "Pages"
},
Fields = new List<CreateSchemaField>
{
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<Guid>(command.SchemaId, command.Name);
await bus.PublishAsync(new ConfigureScripts
{
SchemaId = schemaId.Id,
ScriptCreate = SlugScript,
ScriptUpdate = SlugScript
});
return schemaId;
}
}
}

11
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.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -73,6 +74,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
return this; return this;
} }
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotCreated() private void VerifyNotCreated()
{ {
if (!string.IsNullOrWhiteSpace(Snapshot.FileName)) if (!string.IsNullOrWhiteSpace(Snapshot.FileName))

2
src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetAggregateCommand.cs → src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetCommand.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets.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; } public Guid AssetId { get; set; }

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

@ -6,12 +6,15 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class CreateAsset : AssetAggregateCommand public sealed class CreateAsset : AssetCommand, IAppCommand
{ {
public NamedId<Guid> AppId { get; set; }
public AssetFile File { get; set; } public AssetFile File { get; set; }
public ImageInfo ImageInfo { get; set; } public ImageInfo ImageInfo { get; set; }

2
src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class DeleteAsset : AssetAggregateCommand public sealed class DeleteAsset : AssetCommand
{ {
} }
} }

2
src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAsset.cs

@ -7,7 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class RenameAsset : AssetAggregateCommand public sealed class RenameAsset : AssetCommand
{ {
public string FileName { get; set; } public string FileName { get; set; }
} }

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

@ -9,7 +9,7 @@ using Squidex.Infrastructure.Assets;
namespace Squidex.Domain.Apps.Entities.Assets.Commands namespace Squidex.Domain.Apps.Entities.Assets.Commands
{ {
public sealed class UpdateAsset : AssetAggregateCommand public sealed class UpdateAsset : AssetCommand
{ {
public AssetFile File { get; set; } public AssetFile File { get; set; }

46
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;
}
}
}

2
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)) 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)) if (string.Equals(command.FileName, oldName))

5
src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs

@ -5,18 +5,21 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public interface IAssetEntity : public interface IAssetEntity :
IEntity, IEntity,
IEntityWithAppRef,
IEntityWithCreatedBy, IEntityWithCreatedBy,
IEntityWithLastModifiedBy, IEntityWithLastModifiedBy,
IEntityWithVersion, IEntityWithVersion,
IAssetInfo IAssetInfo
{ {
NamedId<Guid> AppId { get; }
string MimeType { get; } string MimeType { get; }
long FileVersion { get; } long FileVersion { get; }

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

@ -14,7 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{ {
public interface IAssetRepository public interface IAssetRepository
{ {
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<string> mimeTypes = null, HashSet<Guid> ids = null, string query = null, int take = 10, int skip = 0); Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, string query = null);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity> FindAssetAsync(Guid id); Task<IAssetEntity> FindAssetAsync(Guid id);
} }

8
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.Core.ValidateContent;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -18,11 +19,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
{ {
public class AssetState : DomainObjectState<AssetState>, public class AssetState : DomainObjectState<AssetState>,
IAssetEntity, IAssetEntity,
IAssetInfo, IAssetInfo
IUpdateableEntityWithAppRef
{ {
[JsonProperty] [JsonProperty]
public Guid AppId { get; set; } public NamedId<Guid> AppId { get; set; }
[JsonProperty] [JsonProperty]
public string FileName { get; set; } public string FileName { get; set; }
@ -61,6 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
SimpleMapper.Map(@event, this); SimpleMapper.Map(@event, this);
TotalSize += @event.FileSize; TotalSize += @event.FileSize;
AppId = @event.AppId;
} }
protected void On(AssetUpdated @event) protected void On(AssetUpdated @event)

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================= // =========================================================================
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
@ -12,5 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
public sealed class ChangeContentStatus : ContentCommand public sealed class ChangeContentStatus : ContentCommand
{ {
public Status Status { get; set; } public Status Status { get; set; }
public Instant? DueTime { get; set; }
} }
} }

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

@ -6,15 +6,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Security.Claims;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.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; } public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId Guid IAggregateCommand.AggregateId

14
src/Squidex.Domain.Apps.Entities/Contents/Commands/CreateContent.cs

@ -5,10 +5,22 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Commands namespace Squidex.Domain.Apps.Entities.Contents.Commands
{ {
public sealed class CreateContent : ContentDataCommand public sealed class CreateContent : ContentDataCommand, ISchemaCommand, IAppCommand
{ {
public NamedId<Guid> AppId { get; set; }
public NamedId<Guid> SchemaId { get; set; }
public bool Publish { get; set; } public bool Publish { get; set; }
public CreateContent()
{
ContentId = Guid.NewGuid();
}
} }
} }

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

Loading…
Cancel
Save