diff --git a/.dockerignore b/.dockerignore
index 9b8b49c2a..5206962bb 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -9,6 +9,7 @@
# Build results
**/bin/
+**/build/
**/obj/
**/publish/
@@ -18,7 +19,7 @@
# NodeJS
**/node_modules/
-# Scripts (should be copied from node_modules on build)
-**/wwwroot/scripts/**/*.*
+**/src/Squidex/Assets/*.*
-**/src/Squidex/appsettings.Development.json
\ No newline at end of file
+**/src/Squidex/appsettings.Development.json
+**/src/Squidex/Properties/launchSettings.json
\ No newline at end of file
diff --git a/.drone.yml b/.drone.yml
index 389a52a4a..c49d6846a 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -1,3 +1,8 @@
+clone:
+ git:
+ image: plugins/git:next
+ pull: true
+
pipeline:
test_pull_request:
image: docker
@@ -43,9 +48,9 @@ pipeline:
image: plugins/slack
template: >
{{#success build.status}}
- build {{build.number}} succeeded. Good job.
+ Squidex build {{build.number}} succeeded. Good job.
{{else}}
- build {{build.number}} failed. Fix me please.
+ Squidex build {{build.number}} failed. Fix me please.
{{/success}}
secrets: [ slack_webhook ]
when:
diff --git a/.gitignore b/.gitignore
index c2f3a2d05..3631589b0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,8 +7,8 @@
.vscode
# Build results
-build/
bin/
+build/
obj/
publish/
@@ -23,4 +23,7 @@ node_modules/
/src/Squidex/appsettings.Development.json
/src/Squidex/Assets
-/src/Squidex/Properties/launchSettings.json
\ No newline at end of file
+/src/Squidex/package-lock.json
+/src/Squidex/Properties/launchSettings.json
+
+/global.json
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30180d367..ac5184aec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,119 @@
# Changelog
+## v1.11.0 - 2018-09-24
+
+### Features
+
+* **API**: Correct handling of `If-None-Match` header to support caching.
+* **Rules**: Major refactoring of action handlers to add new actions with less code.
+* **Rules**: Twitter action to post status update.
+* **Rules**: Prerender.io action to invalidate cache entries for SPA sites.
+* **Contents**: Support IN-queries, like `fileName in ['Logo.jpg', 'Logo.png']`
+* **UI**: Cloning content items.
+* **UI**: Tag input in header to filter by tags when assigning assets to content.
+* **Schemas**: Color picker as additional editor for string fields.
+* **Statistics**: Report api usage and performance per client.
+
+### Bugfixes
+
+* **Clustering / Orleans**: Fixed correct serialization of exceptions, e.g. as validation errors.
+* **Backups**: Always assign the user who started the restore operation as Owner to the app.
+* **UI**: Reset name when a asset or content query is saved.
+* **UI**: Disable spellchecking for tag editor.
+
+## v1.10.0 - 2018-08-29
+
+### Featues
+
+* **Contents**: Introduce `X-Unpublished` header to also get unpublished content.
+* **UI**: General feature to store UI settings.
+* **UI**: Save content queries.
+* **UI**: Save assets queries.
+* **GraphQL**: Endpoint to run multiple queries in parallel with a single request.
+
+## v1.9.0 - 2018-08-19
+
+### Features
+
+* **Scripting**: Override for the slugify method to use single line characters when replacing diacritics.
+* **Docker**: Smaller image size.
+
+## v1.8.0 - 2018-06-30
+
+### Features
+
+* **Schemas**: Singleton schemas (can only have single content)
+
+### Bugfixes
+
+* **UI**: Nested fields got wrong ids and names and could not be saved.
+* **Content**: Ensure that the content api returns content in correct order when querying by ids.
+
+## v1.7.0 - 2018-06-25
+
+* Migration to .NET Core 2.1
+
+## v1.6.2 - 2018-06-23
+
+### Features
+
+* **UI**: Better sortable with improved UX.
+* **Migration**: Increased performance.
+* **Migration**: Disable event handlers during migration.
+
+### Bugfixes
+
+* **Schemas**: Invariant name handling for field names.
+
+## v1.6.1 - 2018-06-22
+
+### Bugfixes
+
+* **MongoDB**: Fixed date time handling.
+
+## v1.6.0 - 2018-06-07
+
+### Features
+
+* **Schemas**: Nested Schemas.
+* **UI**: Migration to RxJS6.
+* **UI**: Migration to Angular6.
+
+## v1.5.0 - 2018-05-20
+
+### Bugfixes
+
+* **UI**: Fixed the pattern selector in field editor.
+
+### Features
+
+* **Content**: Allow to save content updates as draft.
+* **Schemas**: Create folders to group schemas.
+* **UI**: Increased the search input.
+* **UI**: Plugin system for content editors.
+
+## v1.4.1 - 2018-05-02
+
+### Bugfixes
+
+* **Orleans**: Remove orleans dashboard from 8080.
+
+## v1.4.0 - 2018-05-02
+
+### Features
+
+* **UI**: Big refactorings and UI improvements.
+* **Actions**: New log formatter with placeholder for user infos.
+* **Actions**: Azure Queue action.
+* **Actions**: Algolia action.
+* **Actions**: Fastly action.
+* **Backup**: Backup all your data to an archive.
+
## v1.3.0 - 2018-02-17
### Features
-* **Actions**: ElasticSearch action
+* **Actions**: ElasticSearch action.
### Refactorings
diff --git a/Dockerfile b/Dockerfile
index 3895f5fcf..1a5abe4c7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
#
# Stage 1, Prebuild
#
-FROM squidex/aspnetcore-build-phantomjs:2.0.3-jessie as builder
+FROM squidex/dotnet:2.1-sdk-chromium-phantomjs-node as builder
COPY src/Squidex/package.json /tmp/package.json
@@ -24,24 +24,29 @@ RUN dotnet restore \
&& dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj \
&& dotnet test tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj \
- && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj
+ && dotnet test tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj \
+ && dotnet test tests/Squidex.Tests/Squidex.Tests.csproj
# Publish
-RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release
+RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64
#
# Stage 2, Build runtime
#
-FROM microsoft/aspnetcore:2.0.3-jessie
+FROM microsoft/dotnet:2.1-runtime-deps-alpine
# Default AspNetCore directory
WORKDIR /app
-# Copy from nuild stage
-COPY --from=builder /out/ .
+# add libuv
+RUN apk add --no-cache libuv \
+ && ln -s /usr/lib/libuv.so.1 /usr/lib/libuv.so
+
+# Copy from build stage
+COPY --from=builder /out/alpine .
EXPOSE 80
EXPOSE 33333
EXPOSE 40000
-ENTRYPOINT ["dotnet", "Squidex.dll"]
\ No newline at end of file
+ENTRYPOINT ["./Squidex"]
\ No newline at end of file
diff --git a/Dockerfile.build b/Dockerfile.build
index f09cbb2bb..1474e9c8c 100644
--- a/Dockerfile.build
+++ b/Dockerfile.build
@@ -1,30 +1,7 @@
-FROM microsoft/aspnetcore-build:2.0.0
-
-# Install runtime dependencies
-RUN apt-get update \
- && apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
-
- # Install official PhantomJS release
-RUN set -x \
- && apt-get update \
- && apt-get install -y --no-install-recommends \
- && mkdir /srv/var \
- && mkdir /tmp/phantomjs \
- # Download Phantom JS
- && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
- # Copy binaries only
- && mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
- # Create symbol link
- # Clean up
- && apt-get autoremove -y \
- && apt-get clean all \
- && rm -rf /tmp/* /var/lib/apt/lists/*
-
-RUN phantomjs --version
+FROM squidex/dotnet:2.1-sdk-chromium-phantomjs-node as builder
COPY src/Squidex/package.json /tmp/package.json
+
RUN cd /tmp \
&& npm install \
&& npm rebuild node-sass
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 000000000..306b9eced
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Qaisar Ahmad & Sebastian Stehle
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
deleted file mode 100644
index 95317685c..000000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2017 Qaisar Ahmad & Sebastian Stehle
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
diff --git a/NuGet.Config b/NuGet.Config
deleted file mode 100644
index c982f5720..000000000
--- a/NuGet.Config
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Nuget.config b/Nuget.config
new file mode 100644
index 000000000..8c24b91a1
--- /dev/null
+++ b/Nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index e8bd15355..eb5e7c5b6 100644
--- a/README.md
+++ b/README.md
@@ -4,21 +4,24 @@
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.
-[](https://gitter.im/squidex-cms/Lobby) [](https://squidex-slack.herokuapp.com/) [](http://build.squidex.io/Squidex/squidex) [](https://app.fossa.io/projects/git%2Bgithub.com%2FSquidex%2Fsquidex?ref=badge_shield)
+[](https://support.squidex.io) [](http://build.squidex.io/Squidex/squidex) [](https://app.fossa.io/projects/git%2Bgithub.com%2FSquidex%2Fsquidex?ref=badge_shield)
Read the docs at [https://docs.squidex.io/](https://docs.squidex.io/) (work in progress) or just check out the code and play around.
+## How to make feature requests or report bugs?
+
+Please join our community forum: https://support.squidex.io
+
## Status
-Current Version 1.1. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
+Current Version 1.10.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites
* [Visual Studio Code](https://code.visualstudio.com/) or [Visual Studio 2017](https://www.visualstudio.com/vs/visual-studio-2017-rc/)
-* [Node.js](https://nodejs.org/en/)
-* [.NET Core SDK](https://www.microsoft.com/net/download/core#/current) (Already part of Visual Studio 2017)
+* [Node.js](https://nodejs.org/en/) (development only)
* [MongoDB](https://www.mongodb.com/)
-* [Redis](https://redis.io/download) (If you want to run Squidex on multiple hosts)
+* [.NET Core SDK](https://www.microsoft.com/net/download/core#/current) (Already part of Visual Studio 2017)
## Contributors
@@ -29,9 +32,10 @@ Current Version 1.1. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
### Contributors
-* [pushrbx](https://pushrbx.net/): Azure Store Support.
+* [pushrbx](https://pushrbx.net/): Azure Store support.
* [cpmstars](https://www.cpmstars.com): Asset support for rich editor.
* [civicplus](https://www.civicplus.com/) ([Avd6977](https://github.com/Avd6977), [dsbegnoce](https://github.com/dsbegnoche)): Google Maps support, custom regex patterns and a lot of small improvements.
+* [razims](https://github.com/razims): GridFS support.
## Contributing
diff --git a/Squidex.ruleset b/Squidex.ruleset
index 6c96f8b6b..20a67c5f9 100644
--- a/Squidex.ruleset
+++ b/Squidex.ruleset
@@ -81,6 +81,7 @@
+
\ No newline at end of file
diff --git a/Squidex.sln b/Squidex.sln
index 67ffac379..6d8cfb71d 100644
--- a/Squidex.sln
+++ b/Squidex.sln
@@ -63,6 +63,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Tests", "tests\Squidex.Tests\Squidex.Tests.csproj", "{7E8CC864-4C6E-496F-A672-9F9AD8874835}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Extensions", "extensions\Squidex.Extensions\Squidex.Extensions.csproj", "{F3C41B82-6A67-409A-B7FE-54543EE4F38B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -321,6 +325,18 @@ Global
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x64.Build.0 = Release|Any CPU
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.ActiveCfg = Release|Any CPU
{7E8CC864-4C6E-496F-A672-9F9AD8874835}.Release|x86.Build.0 = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|x64.Build.0 = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Debug|x86.Build.0 = Debug|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x64.ActiveCfg = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x64.Build.0 = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.ActiveCfg = Release|Any CPU
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -335,7 +351,7 @@ Global
{C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{945871B1-77B8-43FB-B53C-27CF385AB756} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{B51126A8-0D75-4A79-867D-10724EC6AC84} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
- {5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{C9809D59-6665-471E-AD87-5AC624C65892} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{C0D540F0-9158-4528-BFD8-BEAE6EAE45EA} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{F7771E22-47BD-45C4-A133-FD7F1DE27CA0} = {C0D540F0-9158-4528-BFD8-BEAE6EAE45EA}
@@ -349,6 +365,7 @@ Global
{AA003372-CD8D-4DBC-962C-F61E0C93CF05} = {C9809D59-6665-471E-AD87-5AC624C65892}
{7DA5B308-D950-4496-93D5-21D6C4D91644} = {C9809D59-6665-471E-AD87-5AC624C65892}
{A4823E14-C0E5-4A4D-B28F-27424C25C3C7} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
+ {F3C41B82-6A67-409A-B7FE-54543EE4F38B} = {FB8BC3A2-2010-4C3C-A87D-D4A98C05EE52}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {02F2E872-3141-44F5-BD6A-33CD84E9FE08}
diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings
deleted file mode 100644
index ecdd59c86..000000000
--- a/Squidex.sln.DotSettings
+++ /dev/null
@@ -1,50 +0,0 @@
-
- False
- True
- False
- True
- False
- True
-
- False
- True
-
- False
- True
-
-
- False
- True
-
- False
- True
-
-
- False
- True
-
-
- True
-
-
-
-
-
-
-
-
- <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
- <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
- <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile>
- False
- ==========================================================================
- Squidex Headless CMS
-==========================================================================
- Copyright (c) Squidex UG (haftungsbeschraenkt)
- All rights reserved. Licensed under the MIT license.
-==========================================================================
-
- True
- True
- True
-
\ No newline at end of file
diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
new file mode 100644
index 000000000..baff5dc43
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
@@ -0,0 +1,34 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Squidex.Extensions.Actions.Algolia
+{
+ [RuleActionHandler(typeof(AlgoliaActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#0d9bf9",
+ Display = "Populate Algolia index",
+ Description = "Populate and synchronize indices in Algolia for full text search.",
+ ReadMore = "https://www.algolia.com/")]
+ public sealed class AlgoliaAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Application Id", Description = "The application ID.")]
+ public string AppId { get; set; }
+
+ [Required]
+ [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")]
+ public string ApiKey { get; set; }
+
+ [Required]
+ [Display(Name = "Index Name", Description = "THe name of the index.")]
+ public string IndexName { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
new file mode 100644
index 000000000..80d7e23d1
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
@@ -0,0 +1,112 @@
+// ==========================================================================
+// 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.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.Algolia
+{
+ public sealed class AlgoliaActionHandler : RuleActionHandler
+ {
+ private const string DescriptionIgnore = "Ignore";
+
+ private readonly ClientPool<(string AppId, string ApiKey, string IndexName), Index> clients;
+
+ public AlgoliaActionHandler(RuleEventFormatter formatter)
+ : base(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, AlgoliaJob Data) CreateJob(EnrichedEvent @event, AlgoliaAction action)
+ {
+ if (@event is EnrichedContentEvent contentEvent)
+ {
+ var contentId = contentEvent.Id.ToString();
+
+ var ruleDescription = string.Empty;
+ var ruleJob = new AlgoliaJob
+ {
+ AppId = action.AppId,
+ ApiKey = action.ApiKey,
+ ContentId = contentId,
+ IndexName = Format(action.IndexName, @event)
+ };
+
+ if (contentEvent.Type == EnrichedContentEventType.Deleted ||
+ contentEvent.Type == EnrichedContentEventType.Unpublished)
+ {
+ ruleDescription = $"Delete entry from Algolia index: {action.IndexName}";
+ }
+ else
+ {
+ ruleDescription = $"Add entry to Algolia index: {action.IndexName}";
+
+ ruleJob.Content = ToPayload(contentEvent);
+ ruleJob.Content["objectID"] = contentId;
+ }
+
+ return (ruleDescription, ruleJob);
+ }
+
+ return (DescriptionIgnore, new AlgoliaJob());
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AlgoliaJob job)
+ {
+ if (string.IsNullOrWhiteSpace(job.AppId))
+ {
+ return (DescriptionIgnore, null);
+ }
+
+ var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName));
+
+ try
+ {
+ if (job.Content != null)
+ {
+ var response = await index.PartialUpdateObjectAsync(job.Content);
+
+ return (response.ToString(Formatting.Indented), null);
+ }
+ else
+ {
+ var response = await index.DeleteObjectAsync(job.ContentId);
+
+ return (response.ToString(Formatting.Indented), null);
+ }
+ }
+ catch (AlgoliaException ex)
+ {
+ return (ex.Message, ex);
+ }
+ }
+ }
+
+ public sealed class AlgoliaJob
+ {
+ public string AppId { get; set; }
+
+ public string ApiKey { get; set; }
+
+ public string ContentId { get; set; }
+
+ public string IndexName { get; set; }
+
+ public JObject Content { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
new file mode 100644
index 000000000..0ee5de663
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
@@ -0,0 +1,41 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Text.RegularExpressions;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.AzureQueue
+{
+ [RuleActionHandler(typeof(AzureQueueActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#0d9bf9",
+ Display = "Send to Azure Queue",
+ Description = "Send an event to azure queue storage.",
+ ReadMore = "https://azure.microsoft.com/en-us/services/storage/queues/")]
+ public sealed class AzureQueueAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Connection String", Description = "The connection string to the storage account.")]
+ public string ConnectionString { get; set; }
+
+ [Required]
+ [Display(Name = "Queue", Description = "The name of the queue.")]
+ public string Queue { get; set; }
+
+ protected override IEnumerable CustomValidate()
+ {
+ if (!string.IsNullOrWhiteSpace(Queue) && !Regex.IsMatch(Queue, "^[a-z][a-z0-9]{2,}(\\-[a-z0-9]+)*$"))
+ {
+ yield return new ValidationError("Queue must be valid azure queue name.", nameof(Queue));
+ }
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
new file mode 100644
index 000000000..ed0c9d8c9
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
@@ -0,0 +1,68 @@
+// ==========================================================================
+// 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 Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.AzureQueue
+{
+ public sealed class AzureQueueActionHandler : RuleActionHandler
+ {
+ private readonly ClientPool<(string ConnectionString, string QueueName), CloudQueue> clients;
+
+ public AzureQueueActionHandler(RuleEventFormatter formatter)
+ : base(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, AzureQueueJob Data) CreateJob(EnrichedEvent @event, AzureQueueAction action)
+ {
+ var queueName = Format(action.Queue, @event);
+
+ var ruleDescription = $"Send AzureQueueJob to azure queue '{queueName}'";
+ var ruleJob = new AzureQueueJob
+ {
+ QueueConnectionString = action.ConnectionString,
+ QueueName = queueName,
+ MessageBodyV2 = ToEnvelopeJson(@event)
+ };
+
+ return (ruleDescription, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(AzureQueueJob job)
+ {
+ var queue = clients.GetClient((job.QueueConnectionString, job.QueueName));
+
+ await queue.AddMessageAsync(new CloudQueueMessage(job.MessageBodyV2));
+
+ return ("Completed", null);
+ }
+ }
+
+ public sealed class AzureQueueJob
+ {
+ public string QueueConnectionString { get; set; }
+
+ public string QueueName { get; set; }
+
+ public string MessageBodyV2 { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/ClientPool.cs b/extensions/Squidex.Extensions/Actions/ClientPool.cs
new file mode 100644
index 000000000..74208afd3
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/ClientPool.cs
@@ -0,0 +1,50 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Options;
+
+#pragma warning disable RECS0108 // Warns about static fields in generic types
+
+namespace Squidex.Extensions.Actions
+{
+ internal sealed class ClientPool
+ {
+ private static readonly TimeSpan TTL = TimeSpan.FromMinutes(30);
+ private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
+ private readonly Func> factory;
+
+ public ClientPool(Func factory)
+ {
+ this.factory = x => Task.FromResult(factory(x));
+ }
+
+ public ClientPool(Func> factory)
+ {
+ this.factory = factory;
+ }
+
+ public TClient GetClient(TKey key)
+ {
+ return GetClientAsync(key).Result;
+ }
+
+ public async Task GetClientAsync(TKey key)
+ {
+ if (!memoryCache.TryGetValue(key, out var client))
+ {
+ client = await factory(key);
+
+ memoryCache.Set(key, client, TTL);
+ }
+
+ return client;
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
new file mode 100644
index 000000000..4711dec26
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
@@ -0,0 +1,50 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Discourse
+{
+ [RuleActionHandler(typeof(DiscourseActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#eB6121",
+ Display = "Post to discourse",
+ Description = "Create a post or topic at discourse.",
+ ReadMore = "https://www.discourse.org/")]
+ public sealed class DiscourseAction : RuleAction
+ {
+ [AbsoluteUrl]
+ [Required]
+ [Display(Name = "Url", Description = "he url to the discourse server.")]
+ public Uri Url { get; set; }
+
+ [Required]
+ [Display(Name = "Api Key", Description = "The api key to authenticate to your discourse server.")]
+ public string ApiKey { get; set; }
+
+ [Required]
+ [Display(Name = "Api Username", Description = "The api username to authenticate to your discourse server.")]
+ public string ApiUsername { get; set; }
+
+ [Required]
+ [Display(Name = "Text", Description = "The text as markdown.")]
+ public string Text { get; set; }
+
+ [Display(Name = "Title", Description = "The optional title when creating new topics.")]
+ public string Title { get; set; }
+
+ [Display(Name = "Topic", Description = "The optional topic id.")]
+ public int? Topic { get; set; }
+
+ [Display(Name = "Category", Description = "The optional category id.")]
+ public int? Category { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
new file mode 100644
index 000000000..22e9131a1
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
@@ -0,0 +1,84 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.Discourse
+{
+ public sealed class DiscourseActionHandler : RuleActionHandler
+ {
+ private const string DescriptionCreatePost = "Create discourse Post";
+ private const string DescriptionCreateTopic = "Create discourse Topic";
+
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public DiscourseActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, DiscourseJob Data) CreateJob(EnrichedEvent @event, DiscourseAction action)
+ {
+ var url = $"{action.Url.ToString().TrimEnd('/')}/posts.json?api_key={action.ApiKey}&api_username={action.ApiUsername}";
+
+ var json =
+ new JObject(
+ new JProperty("raw", Format(action.Text, @event)),
+ new JProperty("title", Format(action.Title, @event)));
+
+ if (action.Topic.HasValue)
+ {
+ json.Add(new JProperty("topic_id", action.Topic.Value));
+ }
+
+ if (action.Category.HasValue)
+ {
+ json.Add(new JProperty("category", action.Category.Value));
+ }
+
+ var ruleJob = new DiscourseJob
+ {
+ RequestUrl = url,
+ RequestBody = json.ToString()
+ };
+
+ var description =
+ action.Topic.HasValue ?
+ DescriptionCreateTopic :
+ DescriptionCreatePost;
+
+ return (description, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(DiscourseJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
+ {
+ Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
+ };
+
+ return await httpClient.OneWayRequestAsync(request, job.RequestBody);
+ }
+ }
+ }
+
+ public sealed class DiscourseJob
+ {
+ public string RequestUrl { get; set; }
+
+ public string RequestBody { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
new file mode 100644
index 000000000..620f9ef6e
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
@@ -0,0 +1,43 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.ElasticSearch
+{
+ [RuleActionHandler(typeof(ElasticSearchActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#1e5470",
+ Display = "Populate ElasticSearch index",
+ Description = "Populate and synchronize indices in ElasticSearch for full text search.",
+ ReadMore = "https://www.elastic.co/")]
+ public sealed class ElasticSearchAction : RuleAction
+ {
+ [AbsoluteUrl]
+ [Required]
+ [Display(Name = "Host", Description = "The hostname of the elastic search instance or cluster.")]
+ public Uri Host { get; set; }
+
+ [Required]
+ [Display(Name = "Index Name", Description = "The name of the index.")]
+ public string IndexName { get; set; }
+
+ [Required]
+ [Display(Name = "Index Type", Description = "The name of the index type.")]
+ public string IndexType { get; set; }
+
+ [Display(Name = "Username", Description = "The optional username.")]
+ public string Username { get; set; }
+
+ [Display(Name = "Password", Description = "The optional password.")]
+ public string Password { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
new file mode 100644
index 000000000..78655e4fd
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
@@ -0,0 +1,125 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading.Tasks;
+using Elasticsearch.Net;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.ElasticSearch
+{
+ public sealed class ElasticSearchActionHandler : RuleActionHandler
+ {
+ private const string DescriptionIgnore = "Ignore";
+
+ private readonly ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient> clients;
+
+ public ElasticSearchActionHandler(RuleEventFormatter formatter)
+ : base(formatter)
+ {
+ clients = new ClientPool<(Uri Host, string Username, string Password), ElasticLowLevelClient>(key =>
+ {
+ var config = new ConnectionConfiguration(key.Host);
+
+ if (!string.IsNullOrEmpty(key.Username) && !string.IsNullOrWhiteSpace(key.Password))
+ {
+ config = config.BasicAuthentication(key.Username, key.Password);
+ }
+
+ return new ElasticLowLevelClient(config);
+ });
+ }
+
+ protected override (string Description, ElasticSearchJob Data) CreateJob(EnrichedEvent @event, ElasticSearchAction action)
+ {
+ if (@event is EnrichedContentEvent contentEvent)
+ {
+ var contentId = contentEvent.Id.ToString();
+
+ var ruleDescription = string.Empty;
+ var ruleJob = new ElasticSearchJob
+ {
+ Host = action.Host.ToString(),
+ ContentId = contentId,
+ IndexName = Format(action.IndexName, @event),
+ IndexType = Format(action.IndexType, @event)
+ };
+
+ if (contentEvent.Type == EnrichedContentEventType.Deleted ||
+ contentEvent.Type == EnrichedContentEventType.Unpublished)
+ {
+ ruleDescription = $"Delete entry index: {action.IndexName}";
+ }
+ else
+ {
+ ruleDescription = $"Upsert to index: {action.IndexName}";
+
+ ruleJob.Content = ToPayload(contentEvent);
+ ruleJob.Content["objectID"] = contentId;
+ }
+
+ ruleJob.Username = action.Username;
+ ruleJob.Password = action.Password;
+
+ return (ruleDescription, ruleJob);
+ }
+
+ return (DescriptionIgnore, new ElasticSearchJob());
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(ElasticSearchJob job)
+ {
+ if (string.IsNullOrWhiteSpace(job.Host))
+ {
+ return (DescriptionIgnore, null);
+ }
+
+ var client = clients.GetClient((new Uri(job.Host, UriKind.Absolute), job.Username, job.Password));
+
+ try
+ {
+ if (job.Content != null)
+ {
+ var doc = job.Content.ToString();
+
+ var response = await client.IndexAsync(job.IndexName, job.IndexType, job.ContentId, doc);
+
+ return (response.Body, response.OriginalException);
+ }
+ else
+ {
+ var response = await client.DeleteAsync(job.IndexName, job.IndexType, job.ContentId);
+
+ return (response.Body, response.OriginalException);
+ }
+ }
+ catch (ElasticsearchClientException ex)
+ {
+ return (ex.Message, ex);
+ }
+ }
+ }
+
+ public sealed class ElasticSearchJob
+ {
+ public string Host { get; set; }
+
+ public string Username { get; set; }
+
+ public string Password { get; set; }
+
+ public string ContentId { get; set; }
+
+ public string IndexName { get; set; }
+
+ public string IndexType { get; set; }
+
+ public JObject Content { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
new file mode 100644
index 000000000..928dab189
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
@@ -0,0 +1,30 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Squidex.Extensions.Actions.Fastly
+{
+ [RuleActionHandler(typeof(FastlyActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#e23335",
+ Display = "Purge fastly cache",
+ Description = "Remove entries from the fastly CDN cache.",
+ ReadMore = "https://www.fastly.com/")]
+ public sealed class FastlyAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Api Key", Description = "The API key to grant access to Squidex.")]
+ public string ApiKey { get; set; }
+
+ [Required]
+ [Display(Name = "Service Id", Description = "The ID of the fastly service.")]
+ public string ServiceId { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
new file mode 100644
index 000000000..a663a7f74
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
@@ -0,0 +1,67 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Fastly
+{
+ public sealed class FastlyActionHandler : RuleActionHandler
+ {
+ private const string Description = "Purge key in fastly";
+
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public FastlyActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ Guard.NotNull(httpClientFactory, nameof(httpClientFactory));
+
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action)
+ {
+ var ruleJob = new FastlyJob
+ {
+ Key = @event.AggregateId.ToString(),
+ FastlyApiKey = action.ApiKey,
+ FastlyServiceID = action.ServiceId
+ };
+
+ return (Description, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(FastlyJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ httpClient.Timeout = TimeSpan.FromSeconds(2);
+
+ var requestUrl = $"https://api.fastly.com/service/{job.FastlyServiceID}/purge/{job.Key}";
+ var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
+
+ request.Headers.Add("Fastly-Key", job.FastlyApiKey);
+
+ return await httpClient.OneWayRequestAsync(request);
+ }
+ }
+ }
+
+ public sealed class FastlyJob
+ {
+ public string FastlyApiKey { get; set; }
+
+ public string FastlyServiceID { get; set; }
+
+ public string Key { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/HttpHelper.cs b/extensions/Squidex.Extensions/Actions/HttpHelper.cs
new file mode 100644
index 000000000..613c99cbc
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/HttpHelper.cs
@@ -0,0 +1,45 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Squidex.Infrastructure.Http;
+
+namespace Squidex.Extensions.Actions
+{
+ public static class HttpHelper
+ {
+ public static async Task<(string Dump, Exception Exception)> OneWayRequestAsync(this HttpClient client, HttpRequestMessage request, string requestBody = null)
+ {
+ HttpResponseMessage response = null;
+ try
+ {
+ response = await client.SendAsync(request);
+
+ var responseString = await response.Content.ReadAsStringAsync();
+
+ var requestDump = DumpFormatter.BuildDump(request, response, requestBody, responseString);
+
+ Exception ex = null;
+
+ if (!response.IsSuccessStatusCode)
+ {
+ ex = new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode}).");
+ }
+
+ return (requestDump, ex);
+ }
+ catch (Exception ex)
+ {
+ var requestDump = DumpFormatter.BuildDump(request, response, requestBody, ex.ToString());
+
+ return (requestDump, ex);
+ }
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
new file mode 100644
index 000000000..6ee4048bb
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
@@ -0,0 +1,43 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Squidex.Extensions.Actions.Medium
+{
+ [RuleActionHandler(typeof(MediumActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#00ab6c",
+ Display = "Post to Medium",
+ Description = "Create a new story or post at medium.",
+ ReadMore = "https://medium.com/")]
+ public sealed class MediumAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Access Token", Description = "The self issued access token.")]
+ public string AccessToken { get; set; }
+
+ [Required]
+ [Display(Name = "Title", Description = "The title, used for the url.")]
+ public string Title { get; set; }
+
+ [Required]
+ [Display(Name = "Content", Description = "The content, either html or markdown.")]
+ public string Content { get; set; }
+
+ [Display(Name = "Canonical Url", Description = "The original home of this content, if it was originally published elsewhere.")]
+ public string CanonicalUrl { get; set; }
+
+ [Display(Name = "Tags", Description = "The optional comma separated list of tags.")]
+ public string Tags { get; set; }
+
+ [Display(Name = "Is Html", Description = "Indicates whether the content is markdown or html.")]
+ public bool IsHtml { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs b/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
new file mode 100644
index 000000000..9e594c7ec
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
@@ -0,0 +1,131 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Infrastructure.Http;
+
+namespace Squidex.Extensions.Actions.Medium
+{
+ public sealed class MediumActionHandler : RuleActionHandler
+ {
+ private const string Description = "Post to medium";
+
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public MediumActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, MediumJob Data) CreateJob(EnrichedEvent @event, MediumAction action)
+ {
+ var requestBody =
+ new JObject(
+ new JProperty("title", Format(action.Title, @event)),
+ new JProperty("contentFormat", action.IsHtml ? "html" : "markdown"),
+ new JProperty("content", Format(action.Content, @event)),
+ new JProperty("canonicalUrl", Format(action.CanonicalUrl, @event)),
+ new JProperty("tags", ParseTags(@event, action)));
+
+ var ruleJob = new MediumJob { AccessToken = action.AccessToken, RequestBody = requestBody.ToString(Formatting.Indented) };
+
+ return (Description, ruleJob);
+ }
+
+ private JArray ParseTags(EnrichedEvent @event, MediumAction action)
+ {
+ if (string.IsNullOrWhiteSpace(action.Tags))
+ {
+ return null;
+ }
+
+ string[] tags;
+ try
+ {
+ var jsonTags = Format(action.Tags, @event);
+
+ tags = JsonConvert.DeserializeObject(jsonTags);
+ }
+ catch
+ {
+ tags = action.Tags.Split(',');
+ }
+
+ return new JArray(tags);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(MediumJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ httpClient.Timeout = TimeSpan.FromSeconds(4);
+ httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
+ httpClient.DefaultRequestHeaders.Add("Accept-Charset", "utf-8");
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "Squidex Headless CMS");
+
+ string id;
+
+ HttpResponseMessage response = null;
+
+ var meRequest = BuildMeRequest(job);
+ try
+ {
+ response = await httpClient.SendAsync(meRequest);
+
+ var responseString = await response.Content.ReadAsStringAsync();
+ var responseJson = JToken.Parse(responseString);
+
+ id = responseJson["data"]["id"].ToString();
+ }
+ catch (Exception ex)
+ {
+ var requestDump = DumpFormatter.BuildDump(meRequest, response, ex.ToString());
+
+ return (requestDump, ex);
+ }
+
+ return await httpClient.OneWayRequestAsync(BuildPostRequest(job, id), job.RequestBody);
+ }
+ }
+
+ private static HttpRequestMessage BuildPostRequest(MediumJob job, string id)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Post, $"https://api.medium.com/v1/users/{id}/posts")
+ {
+ Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
+ };
+
+ request.Headers.Add("Authorization", $"Bearer {job.AccessToken}");
+
+ return request;
+ }
+
+ private static HttpRequestMessage BuildMeRequest(MediumJob job)
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, "https://api.medium.com/v1/me");
+
+ request.Headers.Add("Authorization", $"Bearer {job.AccessToken}");
+
+ return request;
+ }
+ }
+
+ public sealed class MediumJob
+ {
+ public string RequestBody { get; set; }
+
+ public string AccessToken { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
new file mode 100644
index 000000000..47c37eafa
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
@@ -0,0 +1,30 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Squidex.Extensions.Actions.Prerender
+{
+ [RuleActionHandler(typeof(PrerenderActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#2c3e50",
+ Display = "Recache URL",
+ Description = "Recache a certain URL or cache a URL for the first time.",
+ ReadMore = "https://prerender.io")]
+ public sealed class PrerenderAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Token", Description = "The prerender token from your account.")]
+ public string Token { get; set; }
+
+ [Required]
+ [Display(Name = "Url", Description = "The url to recache.")]
+ public string Url { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
new file mode 100644
index 000000000..e8c722490
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
@@ -0,0 +1,58 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+
+namespace Squidex.Extensions.Actions.Prerender
+{
+ public sealed class PrerenderActionHandler : RuleActionHandler
+ {
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public PrerenderActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, PrerenderJob Data) CreateJob(EnrichedEvent @event, PrerenderAction action)
+ {
+ var url = Format(action.Url, @event);
+
+ var request =
+ new JObject(
+ new JProperty("prerenderToken", action.Token),
+ new JProperty("url", url));
+
+ return ($"Recache {url}", new PrerenderJob { RequestBody = request.ToString() });
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(PrerenderJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ var request = new HttpRequestMessage(HttpMethod.Post, "https://api.prerender.io/recache")
+ {
+ Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
+ };
+
+ return await httpClient.OneWayRequestAsync(request, job.RequestBody);
+ }
+ }
+ }
+
+ public sealed class PrerenderJob
+ {
+ public string RequestBody { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs b/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs
new file mode 100644
index 000000000..c5fa0343f
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs
@@ -0,0 +1,25 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+
+namespace Squidex.Extensions.Actions
+{
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class RuleActionAttribute : Attribute
+ {
+ public string ReadMore { get; set; }
+
+ public string IconImage { get; set; }
+
+ public string IconColor { get; set; }
+
+ public string Display { get; set; }
+
+ public string Description { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs b/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
new file mode 100644
index 000000000..5da96ebf5
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
@@ -0,0 +1,31 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions
+{
+ [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+ public sealed class RuleActionHandlerAttribute : Attribute
+ {
+ public Type HandlerType { get; }
+
+ public RuleActionHandlerAttribute(Type handlerType)
+ {
+ Guard.NotNull(handlerType, nameof(handlerType));
+
+ HandlerType = handlerType;
+
+ if (!typeof(IRuleActionHandler).IsAssignableFrom(handlerType))
+ {
+ throw new ArgumentException($"Handler type must implement {typeof(IRuleActionHandler)}.", nameof(handlerType));
+ }
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleElement.cs b/extensions/Squidex.Extensions/Actions/RuleElement.cs
new file mode 100644
index 000000000..6d7244c01
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/RuleElement.cs
@@ -0,0 +1,26 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+
+namespace Squidex.Extensions.Actions
+{
+ public sealed class RuleElement
+ {
+ public Type Type { get; set; }
+
+ public string ReadMore { get; set; }
+
+ public string IconImage { get; set; }
+
+ public string IconColor { get; set; }
+
+ public string Display { get; set; }
+
+ public string Description { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
new file mode 100644
index 000000000..b8e3dae0a
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
@@ -0,0 +1,100 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions
+{
+ public static class RuleElementRegistry
+ {
+ private const string Suffix = "Action";
+ private static readonly HashSet ActionHandlerTypes = new HashSet();
+ private static readonly Dictionary ActionTypes = new Dictionary();
+ private static readonly Dictionary TriggerTypes = new Dictionary
+ {
+ ["ContentChanged"] = new RuleElement
+ {
+ IconImage = "",
+ IconColor = "#3389ff",
+ Display = "Content changed",
+ Description = "For content changes like created, updated, published, unpublished..."
+ },
+
+ ["AssetChanged"] = new RuleElement
+ {
+ IconImage = "",
+ IconColor = "#3389ff",
+ Display = "Asset changed",
+ Description = "For asset changes like uploaded, updated, renamed, deleted..."
+ }
+ };
+
+ public static IReadOnlyDictionary Triggers
+ {
+ get { return TriggerTypes; }
+ }
+
+ public static IReadOnlyDictionary Actions
+ {
+ get { return ActionTypes; }
+ }
+
+ public static IReadOnlyCollection ActionHandlers
+ {
+ get { return ActionHandlerTypes; }
+ }
+
+ static RuleElementRegistry()
+ {
+ var actionTypes =
+ typeof(RuleElementRegistry).Assembly
+ .GetTypes()
+ .Where(x => typeof(RuleAction).IsAssignableFrom(x))
+ .Where(x => x.GetCustomAttribute() != null)
+ .Where(x => x.GetCustomAttribute() != null)
+ .ToList();
+
+ foreach (var actionType in actionTypes)
+ {
+ var name = actionType.Name;
+
+ if (name.EndsWith(Suffix, StringComparison.Ordinal))
+ {
+ name = name.Substring(0, name.Length - Suffix.Length);
+ }
+
+ var metadata = actionType.GetCustomAttribute();
+
+ ActionTypes[name] =
+ new RuleElement
+ {
+ Type = actionType,
+ Display = metadata.Display,
+ Description = metadata.Description,
+ IconColor = metadata.IconColor,
+ IconImage = metadata.IconImage,
+ ReadMore = metadata.ReadMore
+ };
+
+ ActionHandlerTypes.Add(actionType.GetCustomAttribute().HandlerType);
+ }
+ }
+
+ public static void RegisterTypes(TypeNameRegistry typeNameRegistry)
+ {
+ foreach (var actionType in ActionTypes.Values)
+ {
+ typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
+ }
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
new file mode 100644
index 000000000..792a0f3fc
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
@@ -0,0 +1,33 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Slack
+{
+ [RuleActionHandler(typeof(SlackActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#5c3a58",
+ Display = "Send to Slack",
+ Description = "Create a status update at slack to a channel you define.",
+ ReadMore = "https://slack.com")]
+ public sealed class SlackAction : RuleAction
+ {
+ [AbsoluteUrl]
+ [Required]
+ [Display(Name = "Webhook Url", Description = "The slack webhook url.")]
+ public Uri WebhookUrl { get; set; }
+
+ [Required]
+ [Display(Name = "Text", Description = "The text that is sent as message to slack.")]
+ public string Text { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs b/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
new file mode 100644
index 000000000..aeb40f32c
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
@@ -0,0 +1,71 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Slack
+{
+ public sealed class SlackActionHandler : RuleActionHandler
+ {
+ private const string Description = "Send message to slack";
+
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public SlackActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ Guard.NotNull(httpClientFactory, nameof(httpClientFactory));
+
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action)
+ {
+ var body =
+ new JObject(
+ new JProperty("text", Format(action.Text, @event)));
+
+ var ruleJob = new SlackJob
+ {
+ RequestUrl = action.WebhookUrl.ToString(),
+ RequestBody = body.ToString(Formatting.Indented)
+ };
+
+ return (Description, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(SlackJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ httpClient.Timeout = TimeSpan.FromSeconds(2);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
+ {
+ Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
+ };
+
+ return await httpClient.OneWayRequestAsync(request, job.RequestBody);
+ }
+ }
+ }
+
+ public sealed class SlackJob
+ {
+ public string RequestUrl { get; set; }
+
+ public string RequestBody { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
new file mode 100644
index 000000000..a16f98c6f
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
@@ -0,0 +1,34 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+
+namespace Squidex.Extensions.Actions.Twitter
+{
+ [RuleActionHandler(typeof(TweetActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#1da1f2",
+ Display = "Tweet",
+ Description = "Create a status update at Tweet to a your user account.",
+ ReadMore = "https://twitter.com")]
+ public sealed class TweetAction : RuleAction
+ {
+ [Required]
+ [Display(Name = "Access Token", Description = " The generated access token.")]
+ public string AccessToken { get; set; }
+
+ [Required]
+ [Display(Name = "Access Secret", Description = " The generated access secret.")]
+ public string AccessSecret { get; set; }
+
+ [Required]
+ [Display(Name = "Text", Description = "The text that is sent as tweet to twitter.")]
+ public string Text { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs b/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
new file mode 100644
index 000000000..228d7196d
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
@@ -0,0 +1,66 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Threading.Tasks;
+using CoreTweet;
+using Microsoft.Extensions.Options;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Twitter
+{
+ public sealed class TweetActionHandler : RuleActionHandler
+ {
+ private const string Description = "Send a tweet";
+
+ private readonly TwitterOptions twitterOptions;
+
+ public TweetActionHandler(RuleEventFormatter formatter, IOptions twitterOptions)
+ : base(formatter)
+ {
+ Guard.NotNull(twitterOptions, nameof(twitterOptions));
+
+ this.twitterOptions = twitterOptions.Value;
+ }
+
+ protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action)
+ {
+ var ruleJob = new TweetJob
+ {
+ Text = Format(action.Text, @event),
+ AccessToken = action.AccessToken,
+ AccessSecret = action.AccessSecret
+ };
+
+ return (Description, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(TweetJob job)
+ {
+ var tokens = Tokens.Create(
+ twitterOptions.ClientId,
+ twitterOptions.ClientSecret,
+ job.AccessToken,
+ job.AccessSecret);
+
+ await tokens.Statuses.UpdateAsync(status => job.Text);
+
+ return ($"Tweeted: {job.Text}", null);
+ }
+ }
+
+ public sealed class TweetJob
+ {
+ public string AccessToken { get; set; }
+
+ public string AccessSecret { get; set; }
+
+ public string Text { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs b/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs
new file mode 100644
index 000000000..d602e7099
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Extensions.Actions.Twitter
+{
+ public sealed class TwitterOptions
+ {
+ public string ClientId { get; set; }
+
+ public string ClientSecret { get; set; }
+
+ public bool IsConfigured()
+ {
+ return !string.IsNullOrWhiteSpace(ClientId) && !string.IsNullOrWhiteSpace(ClientSecret);
+ }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
new file mode 100644
index 000000000..e2c260b11
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
@@ -0,0 +1,32 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.ComponentModel.DataAnnotations;
+using Squidex.Domain.Apps.Core.Rules;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Webhook
+{
+ [RuleActionHandler(typeof(WebhookActionHandler))]
+ [RuleAction(
+ IconImage = "",
+ IconColor = "#4bb958",
+ Display = "Send webhook",
+ Description = "Send events like ContentPublished to your webhook.",
+ ReadMore = "https://en.wikipedia.org/wiki/Webhook")]
+ public sealed class WebhookAction : RuleAction
+ {
+ [AbsoluteUrl]
+ [Required]
+ [Display(Name = "Url", Description = "he url to the webhook.")]
+ public Uri Url { get; set; }
+
+ [Display(Name = "Shared Secret", Description = "The shared secret that is used to calculate the signature.")]
+ public string SharedSecret { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs b/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
new file mode 100644
index 000000000..4220820ea
--- /dev/null
+++ b/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
@@ -0,0 +1,72 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Squidex.Domain.Apps.Core.HandleRules;
+using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
+using Squidex.Infrastructure;
+
+namespace Squidex.Extensions.Actions.Webhook
+{
+ public sealed class WebhookActionHandler : RuleActionHandler
+ {
+ private readonly IHttpClientFactory httpClientFactory;
+
+ public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
+ : base(formatter)
+ {
+ Guard.NotNull(httpClientFactory, nameof(httpClientFactory));
+
+ this.httpClientFactory = httpClientFactory;
+ }
+
+ protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action)
+ {
+ var requestBody = ToEnvelopeJson(@event);
+ var requestUrl = Format(action.Url, @event);
+
+ var ruleDescription = $"Send event to webhook '{requestUrl}'";
+ var ruleJob = new WebhookJob
+ {
+ RequestUrl = Format(action.Url.ToString(), @event),
+ RequestSignature = $"{requestBody}{action.SharedSecret}".Sha256Base64(),
+ RequestBody = requestBody
+ };
+
+ return (ruleDescription, ruleJob);
+ }
+
+ protected override async Task<(string Dump, Exception Exception)> ExecuteJobAsync(WebhookJob job)
+ {
+ using (var httpClient = httpClientFactory.CreateClient())
+ {
+ var request = new HttpRequestMessage(HttpMethod.Post, job.RequestUrl)
+ {
+ Content = new StringContent(job.RequestBody, Encoding.UTF8, "application/json")
+ };
+
+ request.Headers.Add("X-Signature", job.RequestSignature);
+ request.Headers.Add("X-Application", "Squidex Webhook");
+ request.Headers.Add("User-Agent", "Squidex Webhook");
+
+ return await httpClient.OneWayRequestAsync(request, job.RequestBody);
+ }
+ }
+ }
+
+ public sealed class WebhookJob
+ {
+ public string RequestUrl { get; set; }
+
+ public string RequestSignature { get; set; }
+
+ public string RequestBody { get; set; }
+ }
+}
diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
new file mode 100644
index 000000000..5e0af3773
--- /dev/null
+++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj
@@ -0,0 +1,33 @@
+
+
+ netstandard2.0
+
+
+ full
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\Squidex.ruleset
+
+
+
+
+
diff --git a/libs/Dockerfile b/libs/Dockerfile
deleted file mode 100644
index c10edc3b2..000000000
--- a/libs/Dockerfile
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM microsoft/aspnetcore-build:2.0.3-jessie
-
-# Install runtime dependencies
-RUN apt-get update \
- && apt-get install -y --no-install-recommends ca-certificates bzip2 libfontconfig \
- && apt-get clean \
- && rm -rf /var/lib/apt/lists/*
-
- # Install official PhantomJS release
-RUN set -x \
- && apt-get update \
- && apt-get install -y --no-install-recommends \
- && mkdir /srv/var \
- && mkdir /tmp/phantomjs \
- # Download Phantom JS
- && curl -L https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar -xj --strip-components=1 -C /tmp/phantomjs \
- # Copy binaries only
- && mv /tmp/phantomjs/bin/phantomjs /usr/local/bin \
- # Create symbol link
- # Clean up
- && apt-get autoremove -y \
- && apt-get clean all \
- && rm -rf /tmp/* /var/lib/apt/lists/*
-
-RUN phantomjs --version
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
index be1dbe7f9..51003278f 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
@@ -17,7 +17,6 @@ namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguagesConfig : IFieldPartitioning
{
- public static readonly LanguagesConfig Empty = new LanguagesConfig(ImmutableDictionary.Empty, null, false);
public static readonly LanguagesConfig English = Build(Language.EN);
private readonly ImmutableDictionary languages;
@@ -111,13 +110,10 @@ namespace Squidex.Domain.Apps.Core.Apps
var newLanguages =
languages.Values.Where(x => x.Language != language)
- .Select(config =>
- {
- return new LanguageConfig(
- config.Language,
- config.IsOptional,
- config.LanguageFallbacks.Except(new[] { language }));
- })
+ .Select(config => new LanguageConfig(
+ config.Language,
+ config.IsOptional,
+ config.LanguageFallbacks.Except(new[] { language })))
.ToImmutableDictionary(x => x.Language);
var newMaster =
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
index 0258998e9..2ebc1d054 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
@@ -25,8 +25,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
- protected ContentData(IDictionary copy, IEqualityComparer comparer)
- : base(copy, comparer)
+ protected ContentData(int capacity, IEqualityComparer comparer)
+ : base(capacity, comparer)
{
}
@@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Contents
{
foreach (var otherValue in source)
{
- var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData());
+ var fieldValue = target.GetOrAddNew(otherValue.Key);
foreach (var value in otherValue.Value)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
index 6398d66d4..0ca33663e 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
@@ -18,8 +18,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
- public IdContentData(IdContentData copy)
- : base(copy, EqualityComparer.Default)
+ public IdContentData(int capacity)
+ : base(capacity, EqualityComparer.Default)
{
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
index b7e4187a5..fd298d5ef 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
@@ -6,7 +6,6 @@
// ==========================================================================
using System;
-using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
@@ -18,8 +17,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
- public NamedContentData(NamedContentData copy)
- : base(copy, EqualityComparer.Default)
+ public NamedContentData(int capacity)
+ : base(capacity, StringComparer.OrdinalIgnoreCase)
{
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
new file mode 100644
index 000000000..9e3900deb
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
@@ -0,0 +1,17 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.Contents
+{
+ public enum StatusChange
+ {
+ Archived,
+ Published,
+ Restored,
+ Unpublished
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
index f6600ede5..8190674f1 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
@@ -45,5 +45,12 @@ namespace Squidex.Domain.Apps.Core
{
return Key;
}
+
+ public static Partitioning FromString(string value)
+ {
+ var isLanguage = string.Equals(value, Language.Key, StringComparison.OrdinalIgnoreCase);
+
+ return isLanguage ? Language : Invariant;
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
deleted file mode 100644
index 33295be4a..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Rules.Actions
-{
- [TypeName(nameof(AlgoliaAction))]
- public sealed class AlgoliaAction : RuleAction
- {
- public string AppId { get; set; }
-
- public string ApiKey { get; set; }
-
- public string IndexName { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
deleted file mode 100644
index fc9178243..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Rules.Actions
-{
- [TypeName(nameof(AzureQueueAction))]
- public sealed class AzureQueueAction : RuleAction
- {
- public string ConnectionString { get; set; }
-
- public string Queue { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs
deleted file mode 100644
index e3623f25a..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Rules.Actions
-{
- [TypeName(nameof(ElasticSearchAction))]
- public sealed class ElasticSearchAction : RuleAction
- {
- public Uri Host { get; set; }
-
- public string Username { get; set; }
-
- public string Password { get; set; }
-
- public string IndexName { get; set; }
-
- public string IndexType { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
deleted file mode 100644
index 2d459d500..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschraenkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Rules.Actions
-{
- [TypeName(nameof(FastlyAction))]
- public sealed class FastlyAction : RuleAction
- {
- public string ApiKey { get; set; }
-
- public string ServiceId { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
deleted file mode 100644
index b669fe104..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Rules.Actions
-{
- [TypeName(nameof(SlackAction))]
- public sealed class SlackAction : RuleAction
- {
- public Uri WebhookUrl { get; set; }
-
- public string Text { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
deleted file mode 100644
index 30a6c0707..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-// ==========================================================================
-// 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(WebhookAction))]
- public sealed class WebhookAction : RuleAction
- {
- public Uri Url { get; set; }
-
- public string SharedSecret { get; set; }
-
- public override T Accept(IRuleActionVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
deleted file mode 100644
index 2ef2e3516..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using Squidex.Domain.Apps.Core.Rules.Actions;
-
-namespace Squidex.Domain.Apps.Core.Rules
-{
- public interface IRuleActionVisitor
- {
- T Visit(AlgoliaAction action);
-
- T Visit(AzureQueueAction action);
-
- T Visit(ElasticSearchAction action);
-
- T Visit(FastlyAction action);
-
- T Visit(SlackAction action);
-
- T Visit(WebhookAction action);
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
index ce63262a9..8cf13f2a3 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
@@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Core.Rules.Json
if (!IsEnabled)
{
- rule.Disable();
+ rule = rule.Disable();
}
return rule;
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
index be7bed2ca..e0f3b3618 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
@@ -38,7 +38,10 @@ namespace Squidex.Domain.Apps.Core.Rules
Guard.NotNull(action, nameof(action));
this.trigger = trigger;
+ this.trigger.Freeze();
+
this.action = action;
+ this.action.Freeze();
}
[Pure]
@@ -69,6 +72,8 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New trigger has another type.", nameof(newTrigger));
}
+ newTrigger.Freeze();
+
return Clone(clone =>
{
clone.trigger = newTrigger;
@@ -85,6 +90,8 @@ namespace Squidex.Domain.Apps.Core.Rules
throw new ArgumentException("New action has another type.", nameof(newAction));
}
+ newAction.Freeze();
+
return Clone(clone =>
{
clone.action = newAction;
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
index caaf409d0..10c8be123 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
@@ -5,10 +5,37 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using Squidex.Infrastructure;
+
namespace Squidex.Domain.Apps.Core.Rules
{
public abstract class RuleAction : Freezable
{
- public abstract T Accept(IRuleActionVisitor visitor);
+ public IEnumerable Validate()
+ {
+ var context = new ValidationContext(this);
+ var errors = new List();
+
+ if (!Validator.TryValidateObject(this, context, errors, true))
+ {
+ foreach (var error in errors)
+ {
+ yield return new ValidationError(error.ErrorMessage, error.MemberNames.ToArray());
+ }
+ }
+
+ foreach (var error in CustomValidate())
+ {
+ yield return error;
+ }
+ }
+
+ protected virtual IEnumerable CustomValidate()
+ {
+ yield break;
+ }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
index 963832075..c83cf9626 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
@@ -6,6 +6,7 @@
// ==========================================================================
using System;
+using Newtonsoft.Json.Linq;
using NodaTime;
namespace Squidex.Domain.Apps.Core.Rules
@@ -28,6 +29,6 @@ namespace Squidex.Domain.Apps.Core.Rules
public Instant Expires { get; set; }
- public RuleJobData ActionData { get; set; }
+ public JObject ActionData { get; set; }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
deleted file mode 100644
index 59fb244d7..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System.Collections.Generic;
-using Newtonsoft.Json.Linq;
-
-namespace Squidex.Domain.Apps.Core.Rules
-{
- public sealed class RuleJobData : Dictionary
- {
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
index 33d073f5a..cfcb516d1 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
@@ -20,5 +20,11 @@ namespace Squidex.Domain.Apps.Core.Rules.Triggers
public bool SendDelete { get; set; }
public bool SendPublish { get; set; }
+
+ public bool SendUnpublish { get; set; }
+
+ public bool SendArchived { get; set; }
+
+ public bool SendRestore { get; set; }
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
new file mode 100644
index 000000000..7a74a39f0
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
@@ -0,0 +1,77 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public sealed class ArrayField : RootField, IArrayField
+ {
+ private FieldCollection fields = FieldCollection.Empty;
+
+ public IReadOnlyList Fields
+ {
+ get { return fields.Ordered; }
+ }
+
+ public IReadOnlyDictionary FieldsById
+ {
+ get { return fields.ById; }
+ }
+
+ public IReadOnlyDictionary FieldsByName
+ {
+ get { return fields.ByName; }
+ }
+
+ public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties)
+ : base(id, name, partitioning, properties)
+ {
+ }
+
+ [Pure]
+ public ArrayField DeleteField(long fieldId)
+ {
+ return Updatefields(f => f.Remove(fieldId));
+ }
+
+ [Pure]
+ public ArrayField ReorderFields(List ids)
+ {
+ return Updatefields(f => f.Reorder(ids));
+ }
+
+ [Pure]
+ public ArrayField AddField(NestedField field)
+ {
+ return Updatefields(f => f.Add(field));
+ }
+
+ [Pure]
+ public ArrayField UpdateField(long fieldId, Func updater)
+ {
+ return Updatefields(f => f.Update(fieldId, updater));
+ }
+
+ private ArrayField Updatefields(Func, FieldCollection> updater)
+ {
+ var newFields = updater(fields);
+
+ if (ReferenceEquals(newFields, fields))
+ {
+ return this;
+ }
+
+ return Clone(clone =>
+ {
+ clone.fields = newFields;
+ });
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
new file mode 100644
index 000000000..f3f6100d9
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
@@ -0,0 +1,40 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ [TypeName("ArrayField")]
+ public sealed class ArrayFieldProperties : FieldProperties
+ {
+ public int? MinItems { get; set; }
+
+ public int? MaxItems { get; set; }
+
+ public override T Accept(IFieldPropertiesVisitor visitor)
+ {
+ return visitor.Visit(this);
+ }
+
+ public override T Accept(IFieldVisitor visitor, IField field)
+ {
+ return visitor.Visit((IArrayField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Array(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs
deleted file mode 100644
index fd769b013..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class AssetsField : Field
- {
- public AssetsField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new AssetsFieldProperties())
- {
- }
-
- public AssetsField(long id, string name, Partitioning partitioning, AssetsFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
index d3ed6270a..62b3be7c1 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(AssetsField))]
+ [TypeName("AssetsField")]
public sealed class AssetsFieldProperties : FieldProperties
{
public bool MustBeImage { get; set; }
@@ -42,9 +42,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new AssetsField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Assets(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.Assets(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs
deleted file mode 100644
index fc590e022..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class BooleanField : Field
- {
- public BooleanField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new BooleanFieldProperties())
- {
- }
-
- public BooleanField(long id, string name, Partitioning partitioning, BooleanFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
index 531507e37..a4a0750a5 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(BooleanField))]
+ [TypeName("BooleanField")]
public sealed class BooleanFieldProperties : FieldProperties
{
public bool? DefaultValue { get; set; }
@@ -23,9 +23,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new BooleanField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Boolean(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.Boolean(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs
deleted file mode 100644
index 5c4cf1a1e..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class DateTimeField : Field
- {
- public DateTimeField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new DateTimeFieldProperties())
- {
- }
-
- public DateTimeField(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
index 8dcdd85d5..efbcad12b 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(DateTimeField))]
+ [TypeName("DateTimeField")]
public sealed class DateTimeFieldProperties : FieldProperties
{
public Instant? MaxValue { get; set; }
@@ -28,9 +28,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new DateTimeField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.DateTime(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.DateTime(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs
deleted file mode 100644
index 073c01a42..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System.Diagnostics.Contracts;
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public abstract class Field : Cloneable
- {
- private readonly long fieldId;
- private readonly Partitioning partitioning;
- private readonly string fieldName;
- private bool isDisabled;
- private bool isHidden;
- private bool isLocked;
-
- public long Id
- {
- get { return fieldId; }
- }
-
- public string Name
- {
- get { return fieldName; }
- }
-
- public bool IsLocked
- {
- get { return isLocked; }
- }
-
- public bool IsHidden
- {
- get { return isHidden; }
- }
-
- public bool IsDisabled
- {
- get { return isDisabled; }
- }
-
- public Partitioning Partitioning
- {
- get { return partitioning; }
- }
-
- public abstract FieldProperties RawProperties { get; }
-
- protected Field(long id, string name, Partitioning partitioning)
- {
- Guard.NotNullOrEmpty(name, nameof(name));
- Guard.NotNull(partitioning, nameof(partitioning));
- Guard.GreaterThan(id, 0, nameof(id));
-
- fieldId = id;
- fieldName = name;
-
- this.partitioning = partitioning;
- }
-
- [Pure]
- public Field Lock()
- {
- return Clone(clone =>
- {
- clone.isLocked = true;
- });
- }
-
- [Pure]
- public Field Hide()
- {
- return Clone(clone =>
- {
- clone.isHidden = true;
- });
- }
-
- [Pure]
- public Field Show()
- {
- return Clone(clone =>
- {
- clone.isHidden = false;
- });
- }
-
- [Pure]
- public Field Disable()
- {
- return Clone(clone =>
- {
- clone.isDisabled = true;
- });
- }
-
- [Pure]
- public Field Enable()
- {
- return Clone(clone =>
- {
- clone.isDisabled = false;
- });
- }
-
- public abstract Field Update(FieldProperties newProperties);
-
- public abstract T Accept(IFieldVisitor visitor);
- }
-}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
new file mode 100644
index 000000000..e1546841d
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
@@ -0,0 +1,161 @@
+// ==========================================================================
+// 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.Collections.Immutable;
+using System.Diagnostics.Contracts;
+using System.Linq;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public sealed class FieldCollection : Cloneable> where T : IField
+ {
+ public static readonly FieldCollection Empty = new FieldCollection();
+
+ private ImmutableArray fieldsOrdered = ImmutableArray.Empty;
+ private ImmutableDictionary fieldsById;
+ private ImmutableDictionary fieldsByName;
+
+ public IReadOnlyList Ordered
+ {
+ get { return fieldsOrdered; }
+ }
+
+ public IReadOnlyDictionary ById
+ {
+ get
+ {
+ if (fieldsById == null)
+ {
+ if (fieldsOrdered.Length == 0)
+ {
+ fieldsById = ImmutableDictionary.Empty;
+ }
+ else
+ {
+ fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
+ }
+ }
+
+ return fieldsById;
+ }
+ }
+
+ public IReadOnlyDictionary ByName
+ {
+ get
+ {
+ if (fieldsByName == null)
+ {
+ if (fieldsOrdered.Length == 0)
+ {
+ fieldsByName = ImmutableDictionary.Empty;
+ }
+ else
+ {
+ fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
+ return fieldsByName;
+ }
+ }
+
+ private FieldCollection()
+ {
+ }
+
+ public FieldCollection(T[] fields)
+ {
+ Guard.NotNull(fields, nameof(fields));
+
+ fieldsOrdered = ImmutableArray.Create(fields);
+ }
+
+ protected override void OnCloned()
+ {
+ fieldsById = null;
+ fieldsByName = null;
+ }
+
+ [Pure]
+ public FieldCollection Remove(long fieldId)
+ {
+ if (!ById.TryGetValue(fieldId, out var field))
+ {
+ return this;
+ }
+
+ return Clone(clone =>
+ {
+ clone.fieldsOrdered = fieldsOrdered.Remove(field);
+ });
+ }
+
+ [Pure]
+ public FieldCollection Reorder(List ids)
+ {
+ Guard.NotNull(ids, nameof(ids));
+
+ if (ids.Count != fieldsOrdered.Length || ids.Any(x => !ById.ContainsKey(x)))
+ {
+ throw new ArgumentException("Ids must cover all fields.", nameof(ids));
+ }
+
+ return Clone(clone =>
+ {
+ clone.fieldsOrdered = fieldsOrdered.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableArray();
+ });
+ }
+
+ [Pure]
+ public FieldCollection Add(T field)
+ {
+ Guard.NotNull(field, nameof(field));
+
+ if (ByName.ContainsKey(field.Name) || ById.ContainsKey(field.Id))
+ {
+ throw new ArgumentException($"A field with name '{field.Name}' and id {field.Id} already exists.", nameof(field));
+ }
+
+ return Clone(clone =>
+ {
+ clone.fieldsOrdered = clone.fieldsOrdered.Add(field);
+ });
+ }
+
+ [Pure]
+ public FieldCollection Update(long fieldId, Func updater)
+ {
+ Guard.NotNull(updater, nameof(updater));
+
+ if (!ById.TryGetValue(fieldId, out var field))
+ {
+ return this;
+ }
+
+ var newField = updater(field);
+
+ if (ReferenceEquals(newField, field))
+ {
+ return this;
+ }
+
+ if (!(newField is T typedField))
+ {
+ throw new InvalidOperationException($"Field must be of type {typeof(T)}");
+ }
+
+ return Clone(clone =>
+ {
+ clone.fieldsOrdered = clone.fieldsOrdered.Replace(field, typedField);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
new file mode 100644
index 000000000..bf34e0911
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
@@ -0,0 +1,158 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public static class FieldExtensions
+ {
+ public static Schema ReorderFields(this Schema schema, List ids, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.ReorderFields(ids);
+ }
+
+ return f;
+ });
+ }
+
+ return schema.ReorderFields(ids);
+ }
+
+ public static Schema DeleteField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.DeleteField(fieldId);
+ }
+
+ return f;
+ });
+ }
+
+ return schema.DeleteField(fieldId);
+ }
+
+ public static Schema LockField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Lock());
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Lock());
+ }
+
+ public static Schema HideField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Hide());
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Hide());
+ }
+
+ public static Schema ShowField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Show());
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Show());
+ }
+
+ public static Schema EnableField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Enable());
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Enable());
+ }
+
+ public static Schema DisableField(this Schema schema, long fieldId, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Disable());
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Disable());
+ }
+
+ public static Schema UpdateField(this Schema schema, long fieldId, FieldProperties properties, long? parentId = null)
+ {
+ if (parentId != null)
+ {
+ return schema.UpdateField(parentId.Value, f =>
+ {
+ if (f is ArrayField arrayField)
+ {
+ return arrayField.UpdateField(fieldId, n => n.Update(properties));
+ }
+
+ return f;
+ });
+ }
+
+ return schema.UpdateField(fieldId, f => f.Update(properties));
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
index 3820bd6c5..a9c8d0421 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
@@ -15,8 +15,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string Placeholder { get; set; }
+ public string EditorUrl { get; set; }
+
public abstract T Accept(IFieldPropertiesVisitor visitor);
- public abstract Field CreateField(long id, string name, Partitioning partitioning);
+ public abstract T Accept(IFieldVisitor visitor, IField field);
+
+ public abstract RootField CreateRootField(long id, string name, Partitioning partitioning);
+
+ public abstract NestedField CreateNestedField(long id, string name);
}
}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
index 7d1a61635..2cc7be0de 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
@@ -7,16 +7,15 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
public sealed class FieldRegistry
{
- private delegate Field FactoryFunction(long id, string name, Partitioning partitioning, FieldProperties properties);
-
private readonly TypeNameRegistry typeNameRegistry;
- private readonly Dictionary fieldsByPropertyType = new Dictionary();
+ private readonly HashSet supportedFields = new HashSet();
public FieldRegistry(TypeNameRegistry typeNameRegistry)
{
@@ -24,39 +23,47 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.typeNameRegistry = typeNameRegistry;
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
- RegisterField();
-
- typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "DateTime");
- typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "References");
+ var types = typeof(FieldRegistry).Assembly.GetTypes().Where(x => x.BaseType == typeof(FieldProperties));
+
+ foreach (var type in types)
+ {
+ RegisterField(type);
+ }
+
+ typeNameRegistry.MapObsolete(typeof(ReferencesFieldProperties), "References");
+ typeNameRegistry.MapObsolete(typeof(DateTimeFieldProperties), "DateTime");
+ }
+
+ private void RegisterField(Type type)
+ {
+ if (supportedFields.Add(type))
+ {
+ typeNameRegistry.Map(type);
+ }
+ }
+
+ public RootField CreateRootField(long id, string name, Partitioning partitioning, FieldProperties properties)
+ {
+ CheckProperties(properties);
+
+ return properties.CreateRootField(id, name, partitioning);
}
- private void RegisterField()
+ public NestedField CreateNestedField(long id, string name, FieldProperties properties)
{
- typeNameRegistry.Map(typeof(T));
+ CheckProperties(properties);
- fieldsByPropertyType[typeof(T)] = (id, name, partitioning, properties) => properties.CreateField(id, name, partitioning);
+ return properties.CreateNestedField(id, name);
}
- public Field CreateField(long id, string name, Partitioning partitioning, FieldProperties properties)
+ private void CheckProperties(FieldProperties properties)
{
Guard.NotNull(properties, nameof(properties));
- var factory = fieldsByPropertyType.GetOrDefault(properties.GetType());
-
- if (factory == null)
+ if (!supportedFields.Contains(properties.GetType()))
{
throw new InvalidOperationException($"The field property '{properties.GetType()}' is not supported.");
}
-
- return factory(id, name, partitioning, properties);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
new file mode 100644
index 000000000..de6e49e28
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
@@ -0,0 +1,226 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public static class Fields
+ {
+ public static RootField Array(long id, string name, Partitioning partitioning, params NestedField[] fields)
+ {
+ var result = new ArrayField(id, name, partitioning, new ArrayFieldProperties());
+
+ if (fields != null)
+ {
+ foreach (var field in fields)
+ {
+ result = result.AddField(field);
+ }
+ }
+
+ return result;
+ }
+
+ public static ArrayField Array(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null)
+ {
+ return new ArrayField(id, name, partitioning, properties ?? new ArrayFieldProperties());
+ }
+
+ public static RootField Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new AssetsFieldProperties());
+ }
+
+ public static RootField Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new BooleanFieldProperties());
+ }
+
+ public static RootField DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new DateTimeFieldProperties());
+ }
+
+ public static RootField Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new GeolocationFieldProperties());
+ }
+
+ public static RootField Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new JsonFieldProperties());
+ }
+
+ public static RootField Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new NumberFieldProperties());
+ }
+
+ public static RootField References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new ReferencesFieldProperties());
+ }
+
+ public static RootField String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new StringFieldProperties());
+ }
+
+ public static RootField Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null)
+ {
+ return new RootField(id, name, partitioning, properties ?? new TagsFieldProperties());
+ }
+
+ public static NestedField Assets(long id, string name, AssetsFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new AssetsFieldProperties());
+ }
+
+ public static NestedField Boolean(long id, string name, BooleanFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new BooleanFieldProperties());
+ }
+
+ public static NestedField DateTime(long id, string name, DateTimeFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new DateTimeFieldProperties());
+ }
+
+ public static NestedField Geolocation(long id, string name, GeolocationFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new GeolocationFieldProperties());
+ }
+
+ public static NestedField Json(long id, string name, JsonFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new JsonFieldProperties());
+ }
+
+ public static NestedField Number(long id, string name, NumberFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new NumberFieldProperties());
+ }
+
+ public static NestedField References(long id, string name, ReferencesFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new ReferencesFieldProperties());
+ }
+
+ public static NestedField String(long id, string name, StringFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new StringFieldProperties());
+ }
+
+ public static NestedField Tags(long id, string name, TagsFieldProperties properties = null)
+ {
+ return new NestedField(id, name, properties ?? new TagsFieldProperties());
+ }
+
+ public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler, ArrayFieldProperties properties = null)
+ {
+ var field = Array(id, name, partitioning, properties);
+
+ if (handler != null)
+ {
+ field = handler(field);
+ }
+
+ return schema.AddField(field);
+ }
+
+ public static Schema AddAssets(this Schema schema, long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
+ {
+ return schema.AddField(Assets(id, name, partitioning, properties));
+ }
+
+ public static Schema AddBoolean(this Schema schema, long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null)
+ {
+ return schema.AddField(Boolean(id, name, partitioning, properties));
+ }
+
+ public static Schema AddDateTime(this Schema schema, long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null)
+ {
+ return schema.AddField(DateTime(id, name, partitioning, properties));
+ }
+
+ public static Schema AddGeolocation(this Schema schema, long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null)
+ {
+ return schema.AddField(Geolocation(id, name, partitioning, properties));
+ }
+
+ public static Schema AddJson(this Schema schema, long id, string name, Partitioning partitioning, JsonFieldProperties properties = null)
+ {
+ return schema.AddField(Json(id, name, partitioning, properties));
+ }
+
+ public static Schema AddNumber(this Schema schema, long id, string name, Partitioning partitioning, NumberFieldProperties properties = null)
+ {
+ return schema.AddField(Number(id, name, partitioning, properties));
+ }
+
+ public static Schema AddReferences(this Schema schema, long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null)
+ {
+ return schema.AddField(References(id, name, partitioning, properties));
+ }
+
+ public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning, StringFieldProperties properties = null)
+ {
+ return schema.AddField(String(id, name, partitioning, properties));
+ }
+
+ public static Schema AddTags(this Schema schema, long id, string name, Partitioning partitioning, TagsFieldProperties properties = null)
+ {
+ return schema.AddField(Tags(id, name, partitioning, properties));
+ }
+
+ public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties properties = null)
+ {
+ return field.AddField(Assets(id, name, properties));
+ }
+
+ public static ArrayField AddBoolean(this ArrayField field, long id, string name, BooleanFieldProperties properties = null)
+ {
+ return field.AddField(Boolean(id, name, properties));
+ }
+
+ public static ArrayField AddDateTime(this ArrayField field, long id, string name, DateTimeFieldProperties properties = null)
+ {
+ return field.AddField(DateTime(id, name, properties));
+ }
+
+ public static ArrayField AddGeolocation(this ArrayField field, long id, string name, GeolocationFieldProperties properties = null)
+ {
+ return field.AddField(Geolocation(id, name, properties));
+ }
+
+ public static ArrayField AddJson(this ArrayField field, long id, string name, JsonFieldProperties properties = null)
+ {
+ return field.AddField(Json(id, name, properties));
+ }
+
+ public static ArrayField AddNumber(this ArrayField field, long id, string name, NumberFieldProperties properties = null)
+ {
+ return field.AddField(Number(id, name, properties));
+ }
+
+ public static ArrayField AddReferences(this ArrayField field, long id, string name, ReferencesFieldProperties properties = null)
+ {
+ return field.AddField(References(id, name, properties));
+ }
+
+ public static ArrayField AddString(this ArrayField field, long id, string name, StringFieldProperties properties = null)
+ {
+ return field.AddField(String(id, name, properties));
+ }
+
+ public static ArrayField AddTags(this ArrayField field, long id, string name, TagsFieldProperties properties = null)
+ {
+ return field.AddField(Tags(id, name, properties));
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs
deleted file mode 100644
index adfb366a1..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-using System;
-using System.Diagnostics.Contracts;
-using Squidex.Infrastructure;
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public abstract class Field : Field where T : FieldProperties, new()
- {
- private T properties;
-
- public T Properties
- {
- get { return properties; }
- }
-
- public override FieldProperties RawProperties
- {
- get { return properties; }
- }
-
- protected Field(long id, string name, Partitioning partitioning, T properties)
- : base(id, name, partitioning)
- {
- Guard.NotNull(properties, nameof(properties));
-
- this.properties = properties;
- this.properties.Freeze();
- }
-
- [Pure]
- public override Field Update(FieldProperties newProperties)
- {
- var typedProperties = ValidateProperties(newProperties);
-
- return Clone>(clone =>
- {
- clone.properties = typedProperties;
- clone.properties.Freeze();
- });
- }
-
- private T ValidateProperties(FieldProperties newProperties)
- {
- Guard.NotNull(newProperties, nameof(newProperties));
-
- if (!(newProperties is T typedProperties))
- {
- throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties));
- }
-
- return typedProperties;
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs
deleted file mode 100644
index 078c6e18f..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class GeolocationField : Field
- {
- public GeolocationField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new GeolocationFieldProperties())
- {
- }
-
- public GeolocationField(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
index 4cc7b239b..9136b723c 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(GeolocationField))]
+ [TypeName("GeolocationField")]
public sealed class GeolocationFieldProperties : FieldProperties
{
public GeolocationFieldEditor Editor { get; set; }
@@ -19,9 +19,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new GeolocationField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Geolocation(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.Geolocation(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs
new file mode 100644
index 000000000..0ea74cfbd
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs
@@ -0,0 +1,20 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Collections.Generic;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public interface IArrayField : IField
+ {
+ IReadOnlyList Fields { get; }
+
+ IReadOnlyDictionary FieldsById { get; }
+
+ IReadOnlyDictionary FieldsByName { get; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs
new file mode 100644
index 000000000..6cc86239d
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs
@@ -0,0 +1,26 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public interface IField
+ {
+ long Id { get; }
+
+ string Name { get; }
+
+ bool IsLocked { get; }
+
+ bool IsDisabled { get; }
+
+ bool IsHidden { get; }
+
+ FieldProperties RawProperties { get; }
+
+ T Accept(IFieldVisitor visitor);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
index 108269359..c4593a450 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
@@ -9,6 +9,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldPropertiesVisitor
{
+ T Visit(ArrayFieldProperties properties);
+
T Visit(AssetsFieldProperties properties);
T Visit(BooleanFieldProperties properties);
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
index fd4ce2589..67142acc4 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
@@ -9,22 +9,24 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldVisitor
{
- T Visit(AssetsField field);
+ T Visit(IArrayField field);
- T Visit(BooleanField field);
+ T Visit(IField field);
- T Visit(DateTimeField field);
+ T Visit(IField field);
- T Visit(GeolocationField field);
+ T Visit(IField field);
- T Visit(JsonField field);
+ T Visit(IField field);
- T Visit(NumberField field);
+ T Visit(IField field);
- T Visit(ReferencesField field);
+ T Visit(IField field);
- T Visit(StringField field);
+ T Visit(IField field);
- T Visit(TagsField field);
+ T Visit(IField field);
+
+ T Visit(IField field);
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs
new file mode 100644
index 000000000..0430e72ec
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public interface IField : IField
+ {
+ T Properties { get; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs
new file mode 100644
index 000000000..5bacd00eb
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs
@@ -0,0 +1,13 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public interface INestedField : IField
+ {
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs
new file mode 100644
index 000000000..31d9cd05f
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs
@@ -0,0 +1,14 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public interface IRootField : IField
+ {
+ Partitioning Partitioning { get; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
index e4c04fdf8..952ef87f1 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
@@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
+using System.Collections.Generic;
using Newtonsoft.Json;
namespace Squidex.Domain.Apps.Core.Schemas.Json
@@ -15,21 +16,24 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
public long Id { get; set; }
[JsonProperty]
- public bool IsHidden { get; set; }
+ public string Name { get; set; }
[JsonProperty]
- public bool IsLocked { get; set; }
+ public string Partitioning { get; set; }
[JsonProperty]
- public bool IsDisabled { get; set; }
+ public bool IsHidden { get; set; }
[JsonProperty]
- public string Name { get; set; }
+ public bool IsLocked { get; set; }
[JsonProperty]
- public string Partitioning { get; set; }
+ public bool IsDisabled { get; set; }
[JsonProperty]
public FieldProperties Properties { get; set; }
+
+ [JsonProperty]
+ public List Children { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs
new file mode 100644
index 000000000..59b2035e7
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs
@@ -0,0 +1,29 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschraenkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using Newtonsoft.Json;
+
+namespace Squidex.Domain.Apps.Core.Schemas.Json
+{
+ public sealed class JsonNestedFieldModel
+ {
+ [JsonProperty]
+ public long Id { get; set; }
+
+ [JsonProperty]
+ public string Name { get; set; }
+
+ [JsonProperty]
+ public bool IsHidden { get; set; }
+
+ [JsonProperty]
+ public bool IsDisabled { get; set; }
+
+ [JsonProperty]
+ public FieldProperties Properties { get; set; }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
index be5b64a66..37af1b9e6 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
@@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
{
public sealed class JsonSchemaModel
{
- private static readonly Field[] Empty = new Field[0];
+ private static readonly RootField[] Empty = new RootField[0];
[JsonProperty]
public string Name { get; set; }
@@ -38,11 +38,12 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
Properties = schema.Properties;
Fields =
- schema.Fields?.Select(x =>
+ schema.Fields.Select(x =>
new JsonFieldModel
{
Id = x.Id,
Name = x.Name,
+ Children = CreateChildren(x),
IsHidden = x.IsHidden,
IsLocked = x.IsLocked,
IsDisabled = x.IsDisabled,
@@ -53,13 +54,31 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
IsPublished = schema.IsPublished;
}
- public Schema ToSchema(FieldRegistry fieldRegistry)
+ private static List CreateChildren(IField field)
{
- Field[] fields = Empty;
+ if (field is ArrayField arrayField)
+ {
+ return arrayField.Fields.Select(x =>
+ new JsonNestedFieldModel
+ {
+ Id = x.Id,
+ Name = x.Name,
+ IsHidden = x.IsHidden,
+ IsDisabled = x.IsDisabled,
+ Properties = x.RawProperties
+ }).ToList();
+ }
+
+ return null;
+ }
+
+ public Schema ToSchema(FieldRegistry registry)
+ {
+ var fields = Empty;
if (Fields != null)
{
- fields = new Field[Fields.Count];
+ fields = new RootField[Fields.Count];
for (var i = 0; i < fields.Length; i++)
{
@@ -67,7 +86,29 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json
var parititonKey = new Partitioning(fieldModel.Partitioning);
- var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
+ var field = registry.CreateRootField(fieldModel.Id, fieldModel.Name, parititonKey, fieldModel.Properties);
+
+ if (field is ArrayField arrayField && fieldModel.Children?.Count > 0)
+ {
+ foreach (var nestedFieldModel in fieldModel.Children)
+ {
+ var nestedField = registry.CreateNestedField(nestedFieldModel.Id, nestedFieldModel.Name, nestedFieldModel.Properties);
+
+ if (nestedFieldModel.IsHidden)
+ {
+ nestedField = nestedField.Hide();
+ }
+
+ if (nestedFieldModel.IsDisabled)
+ {
+ nestedField = nestedField.Disable();
+ }
+
+ arrayField = arrayField.AddField(nestedField);
+ }
+
+ field = arrayField;
+ }
if (fieldModel.IsDisabled)
{
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs
deleted file mode 100644
index 9651d6ac0..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class JsonField : Field
- {
- public JsonField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new JsonFieldProperties())
- {
- }
-
- public JsonField(long id, string name, Partitioning partitioning, JsonFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
index 25f4beb18..6edb4f80b 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
@@ -9,7 +9,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(JsonField))]
+ [TypeName("JsonField")]
public sealed class JsonFieldProperties : FieldProperties
{
public override T Accept(IFieldPropertiesVisitor visitor)
@@ -17,9 +17,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new JsonField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Json(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.Json(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
new file mode 100644
index 000000000..c958951ad
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
@@ -0,0 +1,106 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Diagnostics.Contracts;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public abstract class NestedField : Cloneable, INestedField
+ {
+ private readonly long fieldId;
+ private readonly string fieldName;
+ private bool isDisabled;
+ private bool isHidden;
+ private bool isLocked;
+
+ public long Id
+ {
+ get { return fieldId; }
+ }
+
+ public string Name
+ {
+ get { return fieldName; }
+ }
+
+ public bool IsLocked
+ {
+ get { return isLocked; }
+ }
+
+ public bool IsHidden
+ {
+ get { return isHidden; }
+ }
+
+ public bool IsDisabled
+ {
+ get { return isDisabled; }
+ }
+
+ public abstract FieldProperties RawProperties { get; }
+
+ protected NestedField(long id, string name)
+ {
+ Guard.NotNullOrEmpty(name, nameof(name));
+ Guard.GreaterThan(id, 0, nameof(id));
+
+ fieldId = id;
+ fieldName = name;
+ }
+
+ [Pure]
+ public NestedField Lock()
+ {
+ return Clone(clone =>
+ {
+ clone.isLocked = true;
+ });
+ }
+
+ [Pure]
+ public NestedField Hide()
+ {
+ return Clone(clone =>
+ {
+ clone.isHidden = true;
+ });
+ }
+
+ [Pure]
+ public NestedField Show()
+ {
+ return Clone(clone =>
+ {
+ clone.isHidden = false;
+ });
+ }
+
+ [Pure]
+ public NestedField Disable()
+ {
+ return Clone(clone =>
+ {
+ clone.isDisabled = true;
+ });
+ }
+
+ [Pure]
+ public NestedField Enable()
+ {
+ return Clone(clone =>
+ {
+ clone.isDisabled = false;
+ });
+ }
+
+ public abstract T Accept(IFieldVisitor visitor);
+
+ public abstract NestedField Update(FieldProperties newProperties);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
new file mode 100644
index 000000000..7de914a4b
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
@@ -0,0 +1,70 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Diagnostics.Contracts;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public class NestedField : NestedField, IField where T : FieldProperties, new()
+ {
+ private T properties;
+
+ public T Properties
+ {
+ get { return properties; }
+ }
+
+ public override FieldProperties RawProperties
+ {
+ get { return properties; }
+ }
+
+ public NestedField(long id, string name, T properties)
+ : base(id, name)
+ {
+ Guard.NotNull(properties, nameof(properties));
+
+ SetProperties(properties);
+ }
+
+ [Pure]
+ public override NestedField Update(FieldProperties newProperties)
+ {
+ var typedProperties = ValidateProperties(newProperties);
+
+ return Clone>(clone =>
+ {
+ clone.SetProperties(typedProperties);
+ });
+ }
+
+ private void SetProperties(T newProperties)
+ {
+ properties = newProperties;
+ properties.Freeze();
+ }
+
+ private T ValidateProperties(FieldProperties newProperties)
+ {
+ Guard.NotNull(newProperties, nameof(newProperties));
+
+ if (!(newProperties is T typedProperties))
+ {
+ throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties));
+ }
+
+ return typedProperties;
+ }
+
+ public override TResult Accept(IFieldVisitor visitor)
+ {
+ return properties.Accept(visitor, this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs
deleted file mode 100644
index 791698441..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class NumberField : Field
- {
- public NumberField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new NumberFieldProperties())
- {
- }
-
- public NumberField(long id, string name, Partitioning partitioning, NumberFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
index 5732601a9..3238aff25 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(NumberField))]
+ [TypeName("NumberField")]
public sealed class NumberFieldProperties : FieldProperties
{
public ImmutableList AllowedValues { get; set; }
@@ -30,9 +30,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new NumberField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.Number(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.Number(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs
deleted file mode 100644
index 51ba26839..000000000
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-// ==========================================================================
-// Squidex Headless CMS
-// ==========================================================================
-// Copyright (c) Squidex UG (haftungsbeschränkt)
-// All rights reserved. Licensed under the MIT license.
-// ==========================================================================
-
-namespace Squidex.Domain.Apps.Core.Schemas
-{
- public sealed class ReferencesField : Field
- {
- public ReferencesField(long id, string name, Partitioning partitioning)
- : base(id, name, partitioning, new ReferencesFieldProperties())
- {
- }
-
- public ReferencesField(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties)
- : base(id, name, partitioning, properties)
- {
- }
-
- public override T Accept(IFieldVisitor visitor)
- {
- return visitor.Visit(this);
- }
- }
-}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
index cc3740bda..98e4bb5ec 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
@@ -10,7 +10,7 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
{
- [TypeName(nameof(ReferencesField))]
+ [TypeName("ReferencesField")]
public sealed class ReferencesFieldProperties : FieldProperties
{
public int? MinItems { get; set; }
@@ -24,9 +24,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
return visitor.Visit(this);
}
- public override Field CreateField(long id, string name, Partitioning partitioning)
+ public override T Accept(IFieldVisitor visitor, IField field)
{
- return new ReferencesField(id, name, partitioning, this);
+ return visitor.Visit((IField)field);
+ }
+
+ public override RootField CreateRootField(long id, string name, Partitioning partitioning)
+ {
+ return Fields.References(id, name, partitioning, this);
+ }
+
+ public override NestedField CreateNestedField(long id, string name)
+ {
+ return Fields.References(id, name, this);
}
}
}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
new file mode 100644
index 000000000..461f60365
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
@@ -0,0 +1,115 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System.Diagnostics.Contracts;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public abstract class RootField : Cloneable, IRootField
+ {
+ private readonly long fieldId;
+ private readonly string fieldName;
+ private readonly Partitioning partitioning;
+ private bool isDisabled;
+ private bool isHidden;
+ private bool isLocked;
+
+ public long Id
+ {
+ get { return fieldId; }
+ }
+
+ public string Name
+ {
+ get { return fieldName; }
+ }
+
+ public bool IsLocked
+ {
+ get { return isLocked; }
+ }
+
+ public bool IsHidden
+ {
+ get { return isHidden; }
+ }
+
+ public bool IsDisabled
+ {
+ get { return isDisabled; }
+ }
+
+ public Partitioning Partitioning
+ {
+ get { return partitioning; }
+ }
+
+ public abstract FieldProperties RawProperties { get; }
+
+ protected RootField(long id, string name, Partitioning partitioning)
+ {
+ Guard.NotNullOrEmpty(name, nameof(name));
+ Guard.GreaterThan(id, 0, nameof(id));
+ Guard.NotNull(partitioning, nameof(partitioning));
+
+ fieldId = id;
+ fieldName = name;
+
+ this.partitioning = partitioning;
+ }
+
+ [Pure]
+ public RootField Lock()
+ {
+ return Clone(clone =>
+ {
+ clone.isLocked = true;
+ });
+ }
+
+ [Pure]
+ public RootField Hide()
+ {
+ return Clone(clone =>
+ {
+ clone.isHidden = true;
+ });
+ }
+
+ [Pure]
+ public RootField Show()
+ {
+ return Clone(clone =>
+ {
+ clone.isHidden = false;
+ });
+ }
+
+ [Pure]
+ public RootField Disable()
+ {
+ return Clone(clone =>
+ {
+ clone.isDisabled = true;
+ });
+ }
+
+ [Pure]
+ public RootField Enable()
+ {
+ return Clone(clone =>
+ {
+ clone.isDisabled = false;
+ });
+ }
+
+ public abstract T Accept(IFieldVisitor visitor);
+
+ public abstract RootField Update(FieldProperties newProperties);
+ }
+}
\ No newline at end of file
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
new file mode 100644
index 000000000..90165643b
--- /dev/null
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
@@ -0,0 +1,70 @@
+// ==========================================================================
+// Squidex Headless CMS
+// ==========================================================================
+// Copyright (c) Squidex UG (haftungsbeschränkt)
+// All rights reserved. Licensed under the MIT license.
+// ==========================================================================
+
+using System;
+using System.Diagnostics.Contracts;
+using Squidex.Infrastructure;
+
+namespace Squidex.Domain.Apps.Core.Schemas
+{
+ public class RootField : RootField, IField where T : FieldProperties, new()
+ {
+ private T properties;
+
+ public T Properties
+ {
+ get { return properties; }
+ }
+
+ public override FieldProperties RawProperties
+ {
+ get { return properties; }
+ }
+
+ public RootField(long id, string name, Partitioning partitioning, T properties)
+ : base(id, name, partitioning)
+ {
+ Guard.NotNull(properties, nameof(properties));
+
+ SetProperties(properties);
+ }
+
+ [Pure]
+ public override RootField Update(FieldProperties newProperties)
+ {
+ var typedProperties = ValidateProperties(newProperties);
+
+ return Clone>(clone =>
+ {
+ clone.SetProperties(typedProperties);
+ });
+ }
+
+ private void SetProperties(T newProperties)
+ {
+ properties = newProperties;
+ properties.Freeze();
+ }
+
+ private T ValidateProperties(FieldProperties newProperties)
+ {
+ Guard.NotNull(newProperties, nameof(newProperties));
+
+ if (!(newProperties is T typedProperties))
+ {
+ throw new ArgumentException($"Properties must be of type '{typeof(T)}", nameof(newProperties));
+ }
+
+ return typedProperties;
+ }
+
+ public override TResult Accept(IFieldVisitor visitor)
+ {
+ return properties.Accept(visitor, this);
+ }
+ }
+}
diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
index ec20d7e8a..c6e187087 100644
--- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
+++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs
@@ -7,9 +7,7 @@
using System;
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Diagnostics.Contracts;
-using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Schemas
@@ -17,9 +15,7 @@ namespace Squidex.Domain.Apps.Core.Schemas
public sealed class Schema : Cloneable
{
private readonly string name;
- private ImmutableArray fieldsOrdered = ImmutableArray.Empty;
- private ImmutableDictionary fieldsById;
- private ImmutableDictionary fieldsByName;
+ private FieldCollection fields = FieldCollection.Empty;
private SchemaProperties properties;
private bool isPublished;
@@ -33,49 +29,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return isPublished; }
}
- public IReadOnlyList Fields
+ public IReadOnlyList Fields
{
- get { return fieldsOrdered; }
+ get { return fields.Ordered; }
}
- public IReadOnlyDictionary FieldsById
+ public IReadOnlyDictionary FieldsById
{
- get
- {
- if (fieldsById == null)
- {
- if (fieldsOrdered.Length == 0)
- {
- fieldsById = ImmutableDictionary.Empty;
- }
- else
- {
- fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
- }
- }
-
- return fieldsById;
- }
+ get { return fields.ById; }
}
- public IReadOnlyDictionary FieldsByName
+ public IReadOnlyDictionary FieldsByName
{
- get
- {
- if (fieldsByName == null)
- {
- if (fieldsOrdered.Length == 0)
- {
- fieldsByName = ImmutableDictionary.Empty;
- }
- else
- {
- fieldsByName = fieldsOrdered.ToImmutableDictionary(x => x.Name);
- }
- }
-
- return fieldsByName;
- }
+ get { return fields.ByName; }
}
public SchemaProperties Properties
@@ -93,21 +59,14 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.properties.Freeze();
}
- public Schema(string name, Field[] fields, SchemaProperties properties, bool isPublished)
+ public Schema(string name, RootField[] fields, SchemaProperties properties, bool isPublished)
: this(name, properties)
{
- Guard.NotNullOrEmpty(name, nameof(name));
Guard.NotNull(fields, nameof(fields));
- this.isPublished = isPublished;
-
- fieldsOrdered = ImmutableArray.Create(fields);
- }
+ this.fields = new FieldCollection