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")
# What is Squidex?
# What is Squidex??
Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. We build it with ASP.NET Core and CQRS and is tested for Windows and Linux on modern browsers.
@ -10,7 +10,7 @@ Read the docs at [https://docs.squidex.io/](https://docs.squidex.io/) (work in p
## Status
Current Version 1.0-beta3. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
Current Version 1.1. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites
@ -22,7 +22,7 @@ Current Version 1.0-beta3. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadma
## Contributors
### Core Team
### Core Team and Founders
* [Qaisar Ahmad](http://www.qaisarahmad.com/) Interaction Designer, Pakistan
* [Sebastian Stehle](https://github.com/SebastianStehle) Software Engineer, Germany (currently Sweden)

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/=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: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;
$FILENAME$&#xD;
Squidex Headless CMS&#xD;
==========================================================================&#xD;
Copyright (c) Squidex Group&#xD;
All rights reserved.&#xD;
Copyright (c) Squidex UG (haftungsbeschraenkt)&#xD;
All rights reserved. Licensed under the MIT license.&#xD;
==========================================================================&#xD;
</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>

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 Squidex.Infrastructure;
namespace Squidex.Infrastructure
namespace Squidex.Domain.Apps.Core
{
public abstract class Freezable
public abstract class Freezable : IFreezable
{
public bool IsFrozen { get; private set; }
private bool isFrozen;
protected void ThrowIfFrozen()
public bool IsFrozen
{
if (IsFrozen)
get { return isFrozen; }
}
protected void CheckIfFrozen()
{
if (isFrozen)
{
throw new InvalidOperationException("Object is frozen");
}
}
public void Freeze()
public virtual void Freeze()
{
IsFrozen = true;
isFrozen = true;
}
}
}

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))]
public sealed class WebhookAction : RuleAction
{
private Uri url;
private string sharedSecret;
public Uri Url { get; set; }
public Uri Url
{
get
{
return url;
}
set
{
ThrowIfFrozen();
url = value;
}
}
public string SharedSecret
{
get
{
return sharedSecret;
}
set
{
ThrowIfFrozen();
sharedSecret = value;
}
}
public string SharedSecret { get; set; }
public override T Accept<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>
{
T Visit(AlgoliaAction action);
T Visit(AzureQueueAction action);
T Visit(FastlyAction action);
T Visit(SlackAction 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>
{
T Visit(AssetChangedTrigger 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.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules
{
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 AggregateId { get; set; }
public string EventName { 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.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules
{
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))]
public sealed class ContentChangedTrigger : RuleTrigger
{
private ImmutableList<ContentChangedTriggerSchema> schemas;
public ImmutableList<ContentChangedTriggerSchema> Schemas { get; set; }
public ImmutableList<ContentChangedTriggerSchema> Schemas
{
get
{
return schemas;
}
set
{
ThrowIfFrozen();
schemas = value;
}
}
public bool HandleAll { get; set; }
public override T Accept<T>(IRuleTriggerVisitor<T> visitor)
{
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
{
public sealed class ContentChangedTriggerSchema
public sealed class ContentChangedTriggerSchema : Freezable
{
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))]
public sealed class AssetsFieldProperties : FieldProperties
{
private bool mustBeImage;
private int? minItems;
private int? maxItems;
private int? minWidth;
private int? maxWidth;
private int? minHeight;
private int? maxHeight;
private int? minSize;
private int? maxSize;
private int? aspectWidth;
private int? aspectHeight;
private ImmutableList<string> allowedExtensions;
public bool MustBeImage { get; set; }
public bool MustBeImage
{
get
{
return mustBeImage;
}
set
{
ThrowIfFrozen();
public int? MinItems { get; set; }
mustBeImage = value;
}
}
public int? MaxItems { get; set; }
public int? MinItems
{
get
{
return minItems;
}
set
{
ThrowIfFrozen();
public int? MinWidth { get; set; }
minItems = value;
}
}
public int? MaxWidth { get; set; }
public int? MaxItems
{
get
{
return maxItems;
}
set
{
ThrowIfFrozen();
public int? MinHeight { get; set; }
maxItems = value;
}
}
public int? MaxHeight { get; set; }
public int? MinWidth
{
get
{
return minWidth;
}
set
{
ThrowIfFrozen();
public int? MinSize { get; set; }
minWidth = value;
}
}
public int? MaxSize { get; set; }
public int? MaxWidth
{
get
{
return maxWidth;
}
set
{
ThrowIfFrozen();
public int? AspectWidth { get; set; }
maxWidth = value;
}
}
public int? AspectHeight { get; set; }
public int? MinHeight
{
get
{
return minHeight;
}
set
{
ThrowIfFrozen();
minHeight = value;
}
}
public int? MaxHeight
{
get
{
return maxHeight;
}
set
{
ThrowIfFrozen();
maxHeight = value;
}
}
public int? MinSize
{
get
{
return minSize;
}
set
{
ThrowIfFrozen();
minSize = value;
}
}
public int? MaxSize
{
get
{
return maxSize;
}
set
{
ThrowIfFrozen();
maxSize = value;
}
}
public int? AspectWidth
{
get
{
return aspectWidth;
}
set
{
ThrowIfFrozen();
aspectWidth = value;
}
}
public int? AspectHeight
{
get
{
return aspectHeight;
}
set
{
ThrowIfFrozen();
aspectHeight = value;
}
}
public ImmutableList<string> AllowedExtensions
{
get
{
return allowedExtensions;
}
set
{
ThrowIfFrozen();
allowedExtensions = value;
}
}
public ImmutableList<string> AllowedExtensions { get; set; }
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))]
public sealed class BooleanFieldProperties : FieldProperties
{
private BooleanFieldEditor editor;
private bool? defaultValue;
public bool? DefaultValue { get; set; }
public bool? DefaultValue
{
get
{
return defaultValue;
}
set
{
ThrowIfFrozen();
defaultValue = value;
}
}
public BooleanFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value;
}
}
public BooleanFieldEditor Editor { get; set; }
public override T Accept<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))]
public sealed class DateTimeFieldProperties : FieldProperties
{
private DateTimeFieldEditor editor;
private DateTimeCalculatedDefaultValue? calculatedDefaultValue;
private Instant? maxValue;
private Instant? minValue;
private Instant? defaultValue;
public Instant? MaxValue { get; set; }
public Instant? MaxValue
{
get
{
return maxValue;
}
set
{
ThrowIfFrozen();
maxValue = value;
}
}
public Instant? MinValue
{
get
{
return minValue;
}
set
{
ThrowIfFrozen();
minValue = value;
}
}
public Instant? DefaultValue
{
get
{
return defaultValue;
}
set
{
ThrowIfFrozen();
defaultValue = value;
}
}
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue
{
get
{
return calculatedDefaultValue;
}
set
{
ThrowIfFrozen();
public Instant? MinValue { get; set; }
calculatedDefaultValue = value;
}
}
public Instant? DefaultValue { get; set; }
public DateTimeFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; }
editor = value;
}
}
public DateTimeFieldEditor Editor { get; set; }
public override T Accept<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
{
private bool isRequired;
private bool isListField;
private string placeholder;
public bool IsRequired { get; set; }
public bool IsRequired
{
get
{
return isRequired;
}
set
{
ThrowIfFrozen();
public bool IsListField { get; set; }
isRequired = value;
}
}
public bool IsListField
{
get
{
return isListField;
}
set
{
ThrowIfFrozen();
isListField = value;
}
}
public string Placeholder
{
get
{
return placeholder;
}
set
{
ThrowIfFrozen();
placeholder = value;
}
}
public string Placeholder { get; set; }
public abstract T Accept<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))]
public sealed class GeolocationFieldProperties : FieldProperties
{
private GeolocationFieldEditor editor;
public GeolocationFieldEditor Editor
{
get
{
return editor;
}
set
{
ThrowIfFrozen();
editor = value;
}
}
public GeolocationFieldEditor Editor { get; set; }
public override T Accept<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.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public abstract class NamedElementPropertiesBase : Freezable
{
private string label;
private string hints;
public string Label
{
get
{
return label;
}
set
{
ThrowIfFrozen();
label = value;
}
}
public string Hints
{
get
{
return hints;
}
set
{
ThrowIfFrozen();
public string Label { get; set; }
hints = value;
}
}
public string Hints { get; set; }
}
}

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

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

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

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

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

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

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() :
now;
var eventGuid =
@event.Headers.Contains(CommonHeaders.EventId) ?
@event.Headers.EventId() :
var aggregateId =
@event.Headers.Contains(CommonHeaders.AggregateId) ?
@event.Headers.AggregateId() :
Guid.NewGuid();
var job = new RuleJob
{
JobId = eventGuid,
JobId = Guid.NewGuid(),
ActionName = actionName,
ActionData = actionData.Data,
AggregateId = aggregateId,
AppId = appEvent.AppId.Id,
Created = now,
EventName = eventName,

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)
{
if (trigger.HandleAll)
{
return true;
}
if (trigger.Schemas != null && @event.Payload is SchemaEvent schemaEvent)
{
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();
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()

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())
{
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();

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

@ -115,11 +115,11 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
catch (ParserException ex)
{
throw new ValidationException("Failed to execute script with javascript syntaxs error.", new ValidationError(ex.Message));
throw new ValidationException($"Failed to execute script with javascript syntax error: {ex.Message}", new ValidationError(ex.Message));
}
catch (JavaScriptException ex)
{
throw new ValidationException("Failed to execute script with javascript error.", new ValidationError(ex.Message));
throw new ValidationException($"Failed to execute script with javascript error: {ex.Message}", new ValidationError(ex.Message));
}
}
@ -150,6 +150,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
}
engine.SetValue("ctx", contextInstance);
engine.SetValue("slugify", new Func<string, string>(x => x.Slugify()));
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="4.2.1" />
<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="NJsonSchema" Version="9.10.15" />
<PackageReference Include="NJsonSchema" Version="9.10.19" />
<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="System.Collections.Immutable" Version="1.4.0" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
<PackageReference Include="WindowsAzure.Storage" Version="8.7.0" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>

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

@ -8,24 +8,74 @@
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed class MongoAssetEntity : IVersionedEntity<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]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
public string FileName { get; set; }
[BsonRequired]
[BsonElement]
public long FileSize { get; set; }
[BsonRequired]
public AssetState State { get; set; }
[BsonElement]
public long FileVersion { get; set; }
[BsonRequired]
[BsonElement]
public bool IsImage { get; set; }
[BsonRequired]
[BsonElement]
public long Version { get; set; }
[BsonRequired]
[BsonElement]
public int? PixelWidth { get; set; }
[BsonRequired]
[BsonElement]
public int? PixelHeight { get; set; }
[BsonRequired]
[BsonElement]
public RefToken CreatedBy { get; set; }
[BsonRequired]
[BsonElement]
public RefToken LastModifiedBy { get; set; }
[BsonElement]
public bool IsDeleted { get; set; }
Guid IAssetInfo.AssetId
{
get { return Id; }
}
}
}

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

@ -9,10 +9,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Edm;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
@ -34,44 +35,63 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
return collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.State.AppId)
.Ascending(x => x.State.IsDeleted)
.Ascending(x => x.State.FileName)
.Ascending(x => x.State.MimeType)
.Descending(x => x.State.LastModified));
.Ascending(x => x.AppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.FileName)
.Descending(x => x.LastModified));
}
public async Task<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),
Filter.Eq(x => x.State.IsDeleted, false)
};
var odataQuery = EdmAssetModel.Edm.ParseQuery(query);
if (ids != null && ids.Count > 0)
var filter = FindExtensions.BuildQuery(odataQuery, appId);
var contentCount = Collection.Find(filter).CountAsync();
var contentItems =
Collection.Find(filter)
.AssetTake(odataQuery)
.AssetSkip(odataQuery)
.AssetSort(odataQuery)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
return ResultList.Create<IAssetEntity>(contentItems.Result, contentCount.Result);
}
catch (NotSupportedException)
{
filters.Add(Filter.In(x => x.Id, ids));
throw new ValidationException("This odata operation is not supported.");
}
if (mimeTypes != null && mimeTypes.Count > 0)
catch (NotImplementedException)
{
filters.Add(Filter.In(x => x.State.MimeType, mimeTypes));
throw new ValidationException("This odata operation is not supported.");
}
if (!string.IsNullOrWhiteSpace(query))
catch (MongoQueryException ex)
{
filters.Add(Filter.Regex(x => x.State.FileName, new BsonRegularExpression(query, "i")));
if (ex.Message.Contains("17406"))
{
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
}
else
{
throw;
}
}
}
var filter = Filter.And(filters);
public async Task<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 assetCount = Collection.Find(filter).CountAsync();
var assetItems = find.ToListAsync();
var assetCount = find.CountAsync();
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)
@ -80,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.Id == id)
.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 Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
@ -25,15 +25,20 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (existing != null)
{
return (existing.State, existing.Version);
return (SimpleMapper.Map(existing, new AssetState()), existing.Version);
}
return (null, EtagVersion.NotFound);
}
public Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
return Collection.UpsertVersionedAsync(key, oldVersion, newVersion, u => u.Set(x => x.State, value));
var entity = SimpleMapper.Map(value, new MongoAssetEntity());
entity.Version = newVersion;
entity.AppIdId = value.AppId.Id;
await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert);
}
}
}

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]
[BsonElement("ai")]
[BsonRepresentation(BsonType.String)]
public Guid AppId { get; set; }
public Guid AppIdId { get; set; }
[BsonRequired]
[BsonElement("si")]
[BsonRepresentation(BsonType.String)]
public Guid SchemaId { get; set; }
public Guid SchemaIdId { get; set; }
[BsonRequired]
[BsonElement("rf")]
@ -62,6 +62,26 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
[BsonJson]
public IdContentData DataByIds { get; set; }
[BsonRequired]
[BsonElement("ai2")]
public NamedId<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]
[BsonElement("ct")]
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 Microsoft.OData.UriParser;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents;
@ -49,6 +50,12 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
protected override async Task SetupCollectionAsync(IMongoCollection<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(
Index
.Ascending(x => x.Id)
@ -56,30 +63,45 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.SchemaId)
.Text(x => x.DataText)
.Ascending(x => x.SchemaIdId)
.Ascending(x => x.Status)
.Ascending(x => x.IsDeleted)
.Text(x => x.DataText));
await collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.Id)
.Ascending(x => x.IsDeleted));
await collection.Indexes.CreateOneAsync(
Index
.Ascending(x => x.SchemaIdId)
.Ascending(x => x.Id)
.Ascending(x => x.Version));
.Ascending(x => x.IsDeleted)
.Ascending(x => x.Status));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
}
public async Task<IResultList<IContentEntity>> QueryAsync(IAppEntity app, ISchemaEntity schema, Status[] status, ODataUriParser odataQuery)
{
FilterDefinition<MongoContentEntity> filter;
try
{
filter = FindExtensions.BuildQuery(odataQuery, schema.Id, schema.SchemaDef, status);
var propertyCalculator = FindExtensions.CreatePropertyCalculator(schema.SchemaDef);
var filter = FindExtensions.BuildQuery(odataQuery, schema.Id, status, propertyCalculator);
var contentCount = Collection.Find(filter).CountAsync();
var contentItems =
Collection.Find(filter)
.ContentTake(odataQuery)
.ContentSkip(odataQuery)
.ContentSort(odataQuery, propertyCalculator)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
{
entity.ParseData(schema.SchemaDef);
}
return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result);
}
catch (NotSupportedException)
{
@ -89,23 +111,22 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
throw new ValidationException("This odata operation is not supported.");
}
var contentItems = Collection.Find(filter).Take(odataQuery).Skip(odataQuery).Sort(odataQuery, schema.SchemaDef).ToListAsync();
var contentCount = Collection.Find(filter).CountAsync();
await Task.WhenAll(contentItems, contentCount);
foreach (var entity in contentItems.Result)
catch (MongoQueryException ex)
{
entity.ParseData(schema.SchemaDef);
if (ex.Message.Contains("17406"))
{
throw new DomainException("Result set is too large to be retrieved. Use $top parameter to reduce the number of items.");
}
else
{
throw;
}
}
return ResultList.Create<IContentEntity>(contentItems.Result, contentCount.Result);
}
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 contentCount = find.CountAsync();
@ -120,13 +141,13 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
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 =
await Collection.Find(x => contentIds.Contains(x.Id) && x.AppId == appId).Only(x => x.Id)
await Collection.Find(x => x.SchemaIdId == schemaId && ids.Contains(x.Id) && x.IsDeleted == false).Only(x => x.Id)
.ToListAsync();
return contentIds.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList();
return ids.Except(contentEntities.Select(x => Guid.Parse(x["id"].AsString))).ToList();
}
public async Task<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)
{
var contentEntity =
await Collection.Find(x => x.Id == id && !x.IsDeleted)
await Collection.Find(x => x.SchemaIdId == schema.Id && x.Id == id && x.IsDeleted == false)
.FirstOrDefaultAsync();
contentEntity?.ParseData(schema.SchemaDef);
@ -151,6 +172,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return contentEntity;
}
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
{
return Collection.Find(x => x.ScheduledAt < now && x.IsDeleted == false)
.ForEachAsync(c =>
{
callback(c);
});
}
public override async Task ClearAsync()
{
await Database.DropCollectionAsync("States_Contents_Archive");

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

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

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

@ -7,10 +7,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OData.UriParser;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.GenerateEdmSchema;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.MongoDb.OData;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
@ -18,64 +23,80 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Visitors
{
private static readonly FilterDefinitionBuilder<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();
if (top.HasValue)
{
cursor = cursor.Limit(Math.Min((int)top.Value, 200));
}
else
return propertyNames =>
{
cursor = cursor.Limit(20);
}
if (propertyNames.Length > 1)
{
var edmName = propertyNames[1].UnescapeEdmField();
if (!schema.FieldsByName.TryGetValue(edmName, out var field))
{
throw new NotSupportedException();
}
propertyNames[1] = field.Id.ToString();
}
if (propertyNames.Length > 0)
{
propertyNames[0] = PropertyMap[propertyNames[0]];
}
var propertyName = string.Join(".", propertyNames);
return cursor;
return propertyName;
};
}
public static IFindFluent<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)
{
cursor = cursor.Skip((int)skip.Value);
}
else
{
cursor = cursor.Skip(null);
}
return cursor;
return sort != null ? cursor.Sort(sort) : cursor.SortByDescending(x => x.LastModified);
}
public static IFindFluent<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>>
{
Filter.Eq(x => x.SchemaId, schemaId),
Filter.Eq(x => x.SchemaIdId, schemaId),
Filter.In(x => x.Status, status),
Filter.Eq(x => x.IsDeleted, false)
};
var filter = FilterBuilder.Build(query, schema);
var filter = query.BuildFilter<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)

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,
IEntity,
IEntityWithAppRef,
IUpdateableEntity,
IUpdateableEntityWithVersion,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithAppRef
IUpdateableEntityWithCreatedBy
{
[BsonElement]
[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)
{
var historyEventEntities =
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
.ToListAsync();
List<MongoHistoryEventEntity> historyEventEntities;
if (!string.IsNullOrWhiteSpace(channelPrefix))
{
historyEventEntities =
await Collection.Find(x => x.AppId == appId && x.Channel == channelPrefix).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
.ToListAsync();
}
else
{
historyEventEntities =
await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Created).ThenByDescending(x => x.Version).Limit(count)
.ToListAsync();
}
return historyEventEntities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
}

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
.Set(x => x.State, value)
.Set(x => x.AppId, value.AppId)
.Set(x => x.AppId, value.AppId.Id)
.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
.Set(x => x.State, value)
.Set(x => x.AppId, value.AppId)
.Set(x => x.AppId, value.AppId.Id)
.Set(x => x.Name, value.Name)
.Set(x => x.IsDeleted, value.IsDeleted));
}

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" />
</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="RefactoringEssentials" Version="5.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
<PackageReference Include="System.ValueTuple" Version="4.4.0" />
</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;
}
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);
if (!IsFound(schema))
if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId)
{
return null;
}

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

@ -45,9 +45,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
this.appPlansBillingManager = appPlansBillingManager;
}
protected Task On(CreateApp command, CommandContext context)
protected async Task On(CreateApp command, CommandContext context)
{
return handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
var app = await handler.CreateSyncedAsync<AppDomainObject>(context, async a =>
{
await GuardApp.CanCreate(command, appProvider);
@ -179,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
else
{
var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, command.AppId.Id, a.Snapshot.Name, command.PlanId);
var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Snapshot.Id, a.Snapshot.Name, command.PlanId);
if (result is PlanChangedResult)
{
@ -193,10 +193,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (!await this.DispatchActionAsync(context.Command, context))
{
await next();
}
await this.DispatchActionAsync(context.Command, context);
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)
{
return this.DispatchFuncAsync(@event.Payload, @event.Headers, (HistoryEventToStore)null);
return this.DispatchFuncAsync(@event.Payload, (HistoryEventToStore)null);
}
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
{
public sealed class AddLanguage : AppAggregateCommand
public sealed class AddLanguage : AppCommand
{
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
{
public sealed class AddPattern : AppAggregateCommand
public sealed class AddPattern : AppCommand
{
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 Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities
namespace Squidex.Domain.Apps.Entities.Apps.Commands
{
public abstract class SchemaAggregateCommand : SchemaCommand, IAggregateCommand
public abstract class AppCommand : SquidexCommand, IAggregateCommand
{
public Guid AppId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return SchemaId.Id; }
get { return AppId; }
}
}
}

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
{
public sealed class AssignContributor : AppAggregateCommand
public sealed class AssignContributor : AppCommand
{
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
{
public sealed class AttachClient : AppAggregateCommand
public sealed class AttachClient : AppCommand
{
public string Id { get; set; }
public string Secret { get; } = RandomHash.New();
public string Secret { get; set; }
public AttachClient()
{
Secret = RandomHash.New();
}
}
}

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

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

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
{
public sealed class CreateApp : SquidexCommand, IAggregateCommand
public sealed class CreateApp : AppCommand, IAggregateCommand
{
public Guid AppId { get; set; }
public string Name { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AppId; }
}
public string Template { get; set; }
public CreateApp()
{

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

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

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

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

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

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

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

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

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
{
public sealed class UpdateClient : AppAggregateCommand
public sealed class UpdateClient : AppCommand
{
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
{
public sealed class UpdateLanguage : AppAggregateCommand
public sealed class UpdateLanguage : AppCommand
{
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
{
public sealed class UpdatePattern : AppAggregateCommand
public sealed class UpdatePattern : AppCommand
{
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))
{
error(new ValidationError("Client id must be defined.", nameof(command.Id)));
error(new ValidationError("Client id is required.", nameof(command.Id)));
}
else if (clients.ContainsKey(command.Id))
{
@ -40,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
if (string.IsNullOrWhiteSpace(command.Id))
{
error(new ValidationError("Client id must be defined.", nameof(command.Id)));
error(new ValidationError("Client id is required.", nameof(command.Id)));
}
});
}
@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
{
if (string.IsNullOrWhiteSpace(command.Id))
{
error(new ValidationError("Client id must be defined.", nameof(command.Id)));
error(new ValidationError("Client id is required.", nameof(command.Id)));
}
if (string.IsNullOrWhiteSpace(command.Name) && command.Permission == null)
{
error(new ValidationError("Either name or permission must be defined.", nameof(command.Name), nameof(command.Permission)));
error(new ValidationError("Either name or permission is required.", nameof(command.Name), nameof(command.Permission)));
}
if (command.Permission.HasValue && !command.Permission.Value.IsEnumValue())

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

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
{
public abstract class AssetAggregateCommand : AppCommand, IAggregateCommand
public abstract class AssetCommand : SquidexCommand, IAggregateCommand
{
public Guid AssetId { get; set; }

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

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

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

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

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

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

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

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

@ -6,15 +6,12 @@
// ==========================================================================
using System;
using System.Security.Claims;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public abstract class ContentCommand : SchemaCommand, IAggregateCommand
public abstract class ContentCommand : SquidexCommand, IAggregateCommand
{
public ClaimsPrincipal User { get; set; }
public Guid ContentId { get; set; }
Guid IAggregateCommand.AggregateId

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

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