Browse Source

Merge remote-tracking branch 'origin/patch-1'

pull/319/head
Alexander Van Dyke 8 years ago
parent
commit
0ce4dc21b9
  1. 7
      .dockerignore
  2. 9
      .drone.yml
  3. 7
      .gitignore
  4. 111
      CHANGELOG.md
  5. 19
      Dockerfile
  6. 27
      Dockerfile.build
  7. 21
      LICENSE
  8. 21
      LICENSE.txt
  9. 7
      NuGet.Config
  10. 7
      Nuget.config
  11. 16
      README.md
  12. 1
      Squidex.ruleset
  13. 19
      Squidex.sln
  14. 50
      Squidex.sln.DotSettings
  15. 34
      extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  16. 112
      extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  17. 41
      extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
  18. 68
      extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
  19. 50
      extensions/Squidex.Extensions/Actions/ClientPool.cs
  20. 50
      extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
  21. 84
      extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  22. 43
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  23. 125
      extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  24. 30
      extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
  25. 67
      extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  26. 45
      extensions/Squidex.Extensions/Actions/HttpHelper.cs
  27. 43
      extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
  28. 131
      extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  29. 30
      extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
  30. 58
      extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  31. 25
      extensions/Squidex.Extensions/Actions/RuleActionAttribute.cs
  32. 31
      extensions/Squidex.Extensions/Actions/RuleActionHandlerAttribute.cs
  33. 26
      extensions/Squidex.Extensions/Actions/RuleElement.cs
  34. 100
      extensions/Squidex.Extensions/Actions/RuleElementRegistry.cs
  35. 33
      extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
  36. 71
      extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  37. 34
      extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
  38. 66
      extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
  39. 21
      extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs
  40. 32
      extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  41. 72
      extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  42. 33
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  43. 25
      libs/Dockerfile
  44. 12
      src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  45. 6
      src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  46. 4
      src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
  47. 5
      src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  48. 17
      src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
  49. 7
      src/Squidex.Domain.Apps.Core.Model/Partitioning.cs
  50. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs
  51. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs
  52. 31
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/ElasticSearchAction.cs
  53. 24
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs
  54. 25
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs
  55. 25
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/WebhookAction.cs
  56. 26
      src/Squidex.Domain.Apps.Core.Model/Rules/IRuleActionVisitor.cs
  57. 2
      src/Squidex.Domain.Apps.Core.Model/Rules/Json/JsonRule.cs
  58. 7
      src/Squidex.Domain.Apps.Core.Model/Rules/Rule.cs
  59. 29
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleAction.cs
  60. 3
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJob.cs
  61. 16
      src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs
  62. 6
      src/Squidex.Domain.Apps.Core.Model/Rules/Triggers/ContentChangedTriggerSchema.cs
  63. 77
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  64. 40
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  65. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs
  66. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  67. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs
  68. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs
  69. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs
  70. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  71. 115
      src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs
  72. 161
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldCollection.cs
  73. 158
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs
  74. 8
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldProperties.cs
  75. 55
      src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRegistry.cs
  76. 226
      src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  77. 61
      src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs
  78. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs
  79. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationFieldProperties.cs
  80. 20
      src/Squidex.Domain.Apps.Core.Model/Schemas/IArrayField.cs
  81. 26
      src/Squidex.Domain.Apps.Core.Model/Schemas/IField.cs
  82. 2
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
  83. 20
      src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
  84. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/IField{T}.cs
  85. 13
      src/Squidex.Domain.Apps.Core.Model/Schemas/INestedField.cs
  86. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/IRootField.cs
  87. 14
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonFieldModel.cs
  88. 29
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonNestedFieldModel.cs
  89. 53
      src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs
  90. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs
  91. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  92. 106
      src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField.cs
  93. 70
      src/Squidex.Domain.Apps.Core.Model/Schemas/NestedField{T}.cs
  94. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs
  95. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs
  96. 27
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs
  97. 16
      src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesFieldProperties.cs
  98. 115
      src/Squidex.Domain.Apps.Core.Model/Schemas/RootField.cs
  99. 70
      src/Squidex.Domain.Apps.Core.Model/Schemas/RootField{T}.cs
  100. 164
      src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs

7
.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
**/src/Squidex/appsettings.Development.json
**/src/Squidex/Properties/launchSettings.json

9
.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:

7
.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
/src/Squidex/package-lock.json
/src/Squidex/Properties/launchSettings.json
/global.json

111
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

19
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"]
ENTRYPOINT ["./Squidex"]

27
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

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

21
LICENSE.txt

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

7
NuGet.Config

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="localfeed" value="libs" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

7
Nuget.config

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="myget.org" value="https://dotnet.myget.org/F/orleans-ci/api/v3/index.json" protocolVersion="3" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

16
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.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=square)](https://gitter.im/squidex-cms/Lobby) [![Slack](https://img.shields.io/badge/chat-on_slack-E01765.svg?style=square)](https://squidex-slack.herokuapp.com/) [![Build Status](http://build.squidex.io/api/badges/Squidex/squidex/status.svg)](http://build.squidex.io/Squidex/squidex) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FSquidex%2Fsquidex.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FSquidex%2Fsquidex?ref=badge_shield)
[![Discourse topics](https://img.shields.io/discourse/https/support.squidex.io/topics.svg)](https://support.squidex.io) [![Build Status](http://build.squidex.io/api/badges/Squidex/squidex/status.svg)](http://build.squidex.io/Squidex/squidex) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FSquidex%2Fsquidex.svg?type=shield)](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

1
Squidex.ruleset

@ -81,6 +81,7 @@
<Rule Id="AD0001" Action="None" />
</Rules>
<Rules AnalyzerId="Roslyn.Core" RuleNamespace="Microsoft.CodeAnalysis.Diagnostics">
<Rule Id="IDE0032" Action="None" />
<Rule Id="IDE0042" Action="None" />
</Rules>
</RuleSet>

19
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}

50
Squidex.sln.DotSettings

@ -1,50 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Djs/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Djs/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ecshtml/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ecshtml/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ecss/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ecss/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ehtml/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ehtml/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ejs/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ejs/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ejson/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ejson/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Escss/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Escss/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ets/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Ets/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002E_002A/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Header/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Header"&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Namespaces/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Namespaces"&gt;&lt;CSOptimizeUsings&gt;&lt;OptimizeUsings&gt;True&lt;/OptimizeUsings&gt;&lt;EmbraceInRegion&gt;False&lt;/EmbraceInRegion&gt;&lt;RegionName&gt;&lt;/RegionName&gt;&lt;/CSOptimizeUsings&gt;&lt;CSUpdateFileHeader&gt;True&lt;/CSUpdateFileHeader&gt;&lt;/Profile&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/CodeCleanup/Profiles/=Typescript/@EntryIndexedValue">&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Profile name="Typescript"&gt;&lt;JsInsertSemicolon&gt;True&lt;/JsInsertSemicolon&gt;&lt;FormatAttributeQuoteDescriptor&gt;True&lt;/FormatAttributeQuoteDescriptor&gt;&lt;CorrectVariableKindsDescriptor&gt;True&lt;/CorrectVariableKindsDescriptor&gt;&lt;VariablesToInnerScopesDescriptor&gt;True&lt;/VariablesToInnerScopesDescriptor&gt;&lt;StringToTemplatesDescriptor&gt;True&lt;/StringToTemplatesDescriptor&gt;&lt;RemoveRedundantQualifiersTs&gt;True&lt;/RemoveRedundantQualifiersTs&gt;&lt;OptimizeImportsTs&gt;True&lt;/OptimizeImportsTs&gt;&lt;/Profile&gt;</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_AFTER_TYPECAST_PARENTHESES/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/FileHeader/FileHeaderText/@EntryValue">==========================================================================&#xD;
Squidex Headless CMS&#xD;
==========================================================================&#xD;
Copyright (c) Squidex UG (haftungsbeschraenkt)&#xD;
All rights reserved. Licensed under the MIT license.&#xD;
==========================================================================&#xD;
</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
</wpf:ResourceDictionary>

34
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16 .842C7.633.842.842 7.625.842 16S7.625 31.158 16 31.158c8.374 0 15.158-6.791 15.158-15.166S24.375.842 16 .842zm0 25.83c-5.898 0-10.68-4.781-10.68-10.68S10.101 5.313 16 5.313s10.68 4.781 10.68 10.679-4.781 10.68-10.68 10.68zm0-19.156v7.956c0 .233.249.388.458.279l7.055-3.663a.312.312 0 0 0 .124-.434 8.807 8.807 0 0 0-7.319-4.447z'/></svg>",
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; }
}
}

112
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<AlgoliaAction, AlgoliaJob>
{
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; }
}
}

41
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M.011 16L0 6.248l12-1.63V16zM14 4.328L29.996 2v14H14zM30 18l-.004 14L14 29.75V18zM12 29.495L.01 27.851.009 18H12z'/></svg>",
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<ValidationError> 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));
}
}
}
}

68
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<AzureQueueAction, AzureQueueJob>
{
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; }
}
}

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

50
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M16.137 0C7.376 0 0 7.037 0 15.721V32l16.134-.016C24.895 31.984 32 24.676 32 15.995S24.888 0 16.137 0zm.336 6.062a9.862 9.862 0 0 1 5.119 1.555l-.038-.023a.747.747 0 0 1 .05.033l-.033-.021c.288.183.529.353.762.534l-.022-.016c.058.044.094.073.131.103l-.018-.014c.218.174.411.34.597.514l-.005-.005a9.48 9.48 0 0 1 .639.655l.009.01c.073.082.154.176.233.272l.014.018c.053.06.116.133.177.206l.013.017-.052-.047-.008-.007c.104.126.218.273.328.423l.02.028.001.001-.001-.001c-.01-.018.005.005.019.028l.024.042c.145.206.301.451.445.704l.025.048c.131.226.273.51.402.801l.025.063a9.504 9.504 0 0 1 .802 3.853c0 5.38-4.401 9.741-9.831 9.741a9.866 9.866 0 0 1-4.106-.888l.061.025-6.39 1.43 1.78-5.672a7.888 7.888 0 0 1-.293-.584l-.025-.061a8.226 8.226 0 0 1-.254-.617l-.022-.068A1.043 1.043 0 0 1 7 19.017l-.022-.067a8.428 8.428 0 0 1-.246-.829l-.014-.067a9.402 9.402 0 0 1-.265-2.248c0-5.381 4.403-9.744 9.834-9.744l.194.002h-.01z'/></svg>",
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; }
}
}

84
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<DiscourseAction, DiscourseJob>
{
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; }
}
}

43
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 28'><path d='M13.427 17.436H4.163C3.827 16.354 3.636 15.2 3.636 14s.182-2.355.527-3.436h15.245c1.891 0 3.418 1.545 3.418 3.445a3.421 3.421 0 0 1-3.418 3.427h-5.982zm-.436 1.146H4.6a11.508 11.508 0 0 0 4.2 4.982 11.443 11.443 0 0 0 15.827-3.209 5.793 5.793 0 0 0-4.173-1.773H12.99zm7.464-9.164a5.794 5.794 0 0 0 4.173-1.773 11.45 11.45 0 0 0-9.536-5.1c-2.327 0-4.491.7-6.3 1.891a11.554 11.554 0 0 0-4.2 4.982h15.864z'/></svg>",
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; }
}
}

125
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<ElasticSearchAction, ElasticSearchJob>
{
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<StringResponse>(job.IndexName, job.IndexType, job.ContentId, doc);
return (response.Body, response.OriginalException);
}
else
{
var response = await client.DeleteAsync<StringResponse>(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; }
}
}

30
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 32'><path d='M10.68.948v1.736h.806v2.6A12.992 12.992 0 0 0 .951 18.051c0 7.178 5.775 12.996 12.9 12.996 7.124 0 12.9-5.819 12.9-12.996-.004-6.332-4.502-11.605-10.455-12.755l-.081-.013V2.684h.807V.948H10.68zm3.53 10.605c3.218.173 5.81 2.713 6.09 5.922v.211h-.734v.737h.734v.201c-.279 3.21-2.871 5.752-6.09 5.925v-.723h-.733v.721c-3.281-.192-5.905-2.845-6.077-6.152h.728v-.737h-.724c.195-3.284 2.808-5.911 6.073-6.103v.725h.733v-.727zm2.513 3.051l-2.462 2.282a1.13 1.13 0 0 0-.41-.078c-.633 0-1.147.517-1.147 1.155a1.15 1.15 0 0 0 1.147 1.155c.633 0 1.147-.517 1.147-1.155 0-.117-.018-.23-.05-.337l.002.008 2.223-2.505-.449-.526z'/></svg>",
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; }
}
}

67
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<FastlyAction, FastlyJob>
{
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; }
}
}

45
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);
}
}
}
}

43
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M3.795 8.48a1.239 1.239 0 0 0-.404-1.045l-2.987-3.6v-.537H9.68l7.171 15.727 6.304-15.727H32v.537l-2.556 2.449a.749.749 0 0 0-.284.717v18a.749.749 0 0 0 .284.716l2.493 2.449v.537H19.39v-.537l2.583-2.509c.253-.253.253-.328.253-.717V10.392l-7.187 18.251h-.969L5.703 10.392v12.232a1.69 1.69 0 0 0 .463 1.404l3.36 4.08v.536H-.001v-.537l3.36-4.08c.36-.371.52-.893.435-1.403V8.48z'/></svg>",
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; }
}
}

131
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<MediumAction, MediumJob>
{
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<string[]>(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; }
}
}

30
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M2.073 17.984l8.646-5.36v-1.787L.356 17.325v1.318l10.363 6.488v-1.787zM29.927 17.984l-8.646-5.36v-1.787l10.363 6.488v1.318l-10.363 6.488v-1.787zM18.228 6.693l-6.276 19.426 1.656.548 6.276-19.426z'/></svg>",
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; }
}
}

58
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<PrerenderAction, PrerenderJob>
{
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; }
}
}

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

31
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));
}
}
}
}

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

100
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<Type> ActionHandlerTypes = new HashSet<Type>();
private static readonly Dictionary<string, RuleElement> ActionTypes = new Dictionary<string, RuleElement>();
private static readonly Dictionary<string, RuleElement> TriggerTypes = new Dictionary<string, RuleElement>
{
["ContentChanged"] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 0 1 0 21.875V6.125A6.087 6.087 0 0 1 6.125 0h15.75A6.087 6.087 0 0 1 28 6.125v15.75A6.088 6.088 0 0 1 21.875 28zM6.125 1.75A4.333 4.333 0 0 0 1.75 6.125v15.75a4.333 4.333 0 0 0 4.375 4.375h15.75a4.333 4.333 0 0 0 4.375-4.375V6.125a4.333 4.333 0 0 0-4.375-4.375H6.125z'/><path d='M13.125 12.25H7.35c-1.575 0-2.888-1.313-2.888-2.888V7.349c0-1.575 1.313-2.888 2.888-2.888h5.775c1.575 0 2.887 1.313 2.887 2.888v2.013c0 1.575-1.312 2.888-2.887 2.888zM7.35 6.212c-.613 0-1.138.525-1.138 1.138v2.012A1.16 1.16 0 0 0 7.35 10.5h5.775a1.16 1.16 0 0 0 1.138-1.138V7.349a1.16 1.16 0 0 0-1.138-1.138H7.35zM22.662 16.713H5.337c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h17.237c.525 0 .875.35.875.875s-.35.875-.787.875zM15.138 21.262h-9.8c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h9.713c.525 0 .875.35.875.875s-.35.875-.787.875z'/></svg>",
IconColor = "#3389ff",
Display = "Content changed",
Description = "For content changes like created, updated, published, unpublished..."
},
["AssetChanged"] = new RuleElement
{
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 0 1 0 21.875V6.125A6.087 6.087 0 0 1 6.125 0h15.75A6.087 6.087 0 0 1 28 6.125v15.75A6.088 6.088 0 0 1 21.875 28zM6.125 1.75A4.333 4.333 0 0 0 1.75 6.125v15.75a4.333 4.333 0 0 0 4.375 4.375h15.75a4.333 4.333 0 0 0 4.375-4.375V6.125a4.333 4.333 0 0 0-4.375-4.375H6.125z'/><path d='M21.088 23.537H9.1c-.35 0-.612-.175-.787-.525s-.088-.7.088-.962l8.225-9.713c.175-.175.438-.35.7-.35s.525.175.7.35l5.25 7.525c.088.087.088.175.088.262.438 1.225.087 2.012-.175 2.45-.613.875-1.925.963-2.1.963zm-10.063-1.75h10.15c.175 0 .612-.088.7-.262.088-.088.088-.35 0-.7l-4.55-6.475-6.3 7.438zM9.1 13.737c-2.1 0-3.85-1.75-3.85-3.85S7 6.037 9.1 6.037s3.85 1.75 3.85 3.85-1.663 3.85-3.85 3.85zm0-5.949c-1.138 0-2.1.875-2.1 2.1s.962 2.1 2.1 2.1 2.1-.962 2.1-2.1-.875-2.1-2.1-2.1z'/></svg>",
IconColor = "#3389ff",
Display = "Asset changed",
Description = "For asset changes like uploaded, updated, renamed, deleted..."
}
};
public static IReadOnlyDictionary<string, RuleElement> Triggers
{
get { return TriggerTypes; }
}
public static IReadOnlyDictionary<string, RuleElement> Actions
{
get { return ActionTypes; }
}
public static IReadOnlyCollection<Type> ActionHandlers
{
get { return ActionHandlerTypes; }
}
static RuleElementRegistry()
{
var actionTypes =
typeof(RuleElementRegistry).Assembly
.GetTypes()
.Where(x => typeof(RuleAction).IsAssignableFrom(x))
.Where(x => x.GetCustomAttribute<RuleActionAttribute>() != null)
.Where(x => x.GetCustomAttribute<RuleActionHandlerAttribute>() != 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<RuleActionAttribute>();
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<RuleActionHandlerAttribute>().HandlerType);
}
}
public static void RegisterTypes(TypeNameRegistry typeNameRegistry)
{
foreach (var actionType in ActionTypes.Values)
{
typeNameRegistry.Map(actionType.Type, actionType.Type.Name);
}
}
}
}

33
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 28'><path d='M23.734 12.125c1.281 0 2.266.938 2.266 2.219 0 1-.516 1.703-1.453 2.031l-2.688.922.875 2.609c.078.234.109.484.109.734 0 1.234-1 2.266-2.234 2.266a2.271 2.271 0 0 1-2.172-1.547l-.859-2.578-4.844 1.656.859 2.562c.078.234.125.484.125.734 0 1.219-1 2.266-2.25 2.266a2.25 2.25 0 0 1-2.156-1.547l-.859-2.547-2.391.828c-.25.078-.516.141-.781.141-1.266 0-2.219-.938-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.438-.828-1.641-4.891-2.438.844c-.25.078-.5.125-.75.125-1.25 0-2.219-.953-2.219-2.203 0-.969.625-1.844 1.547-2.156l2.453-.828-.828-2.484a2.337 2.337 0 0 1-.125-.734c0-1.234 1-2.266 2.25-2.266a2.25 2.25 0 0 1 2.156 1.547l.844 2.5L13.14 5.5 12.296 3a2.337 2.337 0 0 1-.125-.734c0-1.234 1.016-2.266 2.25-2.266.984 0 1.859.625 2.172 1.547l.828 2.516 2.531-.859c.219-.063.438-.094.672-.094 1.219 0 2.266.906 2.266 2.156 0 .969-.75 1.781-1.625 2.078l-2.453.844 1.641 4.937 2.562-.875a2.32 2.32 0 0 1 .719-.125zm-12.406 4.094l4.844-1.641-1.641-4.922-4.844 1.672z'/></svg>",
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; }
}
}

71
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<SlackAction, SlackJob>
{
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; }
}
}

34
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><path d='M32 7.075a12.941 12.941 0 0 1-3.769 1.031 6.601 6.601 0 0 0 2.887-3.631 13.21 13.21 0 0 1-4.169 1.594A6.565 6.565 0 0 0 22.155 4a6.563 6.563 0 0 0-6.563 6.563c0 .512.056 1.012.169 1.494A18.635 18.635 0 0 1 2.23 5.195a6.56 6.56 0 0 0-.887 3.3 6.557 6.557 0 0 0 2.919 5.463 6.565 6.565 0 0 1-2.975-.819v.081a6.565 6.565 0 0 0 5.269 6.437 6.574 6.574 0 0 1-2.968.112 6.588 6.588 0 0 0 6.131 4.563 13.17 13.17 0 0 1-9.725 2.719 18.568 18.568 0 0 0 10.069 2.95c12.075 0 18.681-10.006 18.681-18.681 0-.287-.006-.569-.019-.85A13.216 13.216 0 0 0 32 7.076z'/></svg>",
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; }
}
}

66
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<TweetAction, TweetJob>
{
private const string Description = "Send a tweet";
private readonly TwitterOptions twitterOptions;
public TweetActionHandler(RuleEventFormatter formatter, IOptions<TwitterOptions> 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; }
}
}

21
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);
}
}
}

32
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 = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M5.95 27.125h-.262C1.75 26.425 0 23.187 0 20.3c0-2.713 1.575-5.688 5.075-6.563V9.712c0-.525.35-.875.875-.875s.875.35.875.875v4.725c0 .438-.35.787-.7.875-2.975.438-4.375 2.8-4.375 4.988s1.313 4.55 4.2 5.075h.175a.907.907 0 0 1 .7 1.05c-.088.438-.438.7-.875.7zM21.175 27.387c-2.8 0-5.775-1.662-6.65-5.075H9.712c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h5.512c.438 0 .787.35.875.7.438 2.975 2.8 4.288 4.988 4.375 2.188 0 4.55-1.313 5.075-4.2v-.088a.908.908 0 0 1 1.05-.7.908.908 0 0 1 .7 1.05v.088c-.612 3.85-3.85 5.6-6.737 5.6zM21.525 18.55c-.525 0-.875-.35-.875-.875v-4.813c0-.438.35-.787.7-.875 2.975-.438 4.288-2.8 4.375-4.987 0-2.188-1.313-4.55-4.2-5.075h-.088c-.525-.175-.875-.613-.787-1.05s.525-.788 1.05-.7h.088c3.938.7 5.688 3.937 5.688 6.825 0 2.713-1.662 5.688-5.075 6.563v4.113c0 .438-.438.875-.875.875zM1.137 6.737H.962c-.438-.087-.788-.525-.7-.963v-.087c.7-3.938 3.85-5.688 6.737-5.688h.087c2.712 0 5.688 1.662 6.563 5.075h4.025c.525 0 .875.35.875.875s-.35.875-.875.875h-4.725c-.438 0-.788-.35-.875-.7-.438-2.975-2.8-4.288-4.988-4.375-2.188 0-4.55 1.313-5.075 4.2v.087c-.088.438-.438.7-.875.7z'/><path d='M7 10.588c-.875 0-1.837-.35-2.538-1.05a3.591 3.591 0 0 1 0-5.075C5.162 3.851 6.037 3.5 7 3.5s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.537s-.35 1.837-1.05 2.538c-.7.612-1.575.963-2.537.963zM7 5.25c-.438 0-.875.175-1.225.525a1.795 1.795 0 0 0 2.538 2.538c.35-.35.525-.788.525-1.313s-.175-.875-.525-1.225S7.525 5.25 7 5.25zM21.088 23.887a3.65 3.65 0 0 1-2.537-1.05 3.591 3.591 0 0 1 0-5.075c.7-.7 1.575-1.05 2.537-1.05s1.838.35 2.537 1.05c.7.7 1.05 1.575 1.05 2.538s-.35 1.837-1.05 2.537c-.787.7-1.662 1.05-2.537 1.05zm0-5.337c-.525 0-.963.175-1.313.525a1.795 1.795 0 0 0 2.537 2.538c.35-.35.525-.788.525-1.313s-.175-.963-.525-1.313-.787-.438-1.225-.438zM20.387 10.588c-.875 0-1.837-.35-2.537-1.05S16.8 7.963 16.8 7.001s.35-1.837 1.05-2.538c.7-.612 1.662-.962 2.537-.962s1.838.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.963-2.538.963zm0-5.338c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.962.525 1.313c.7.7 1.838.7 2.538 0s.7-1.838 0-2.538c-.263-.438-.7-.612-1.225-.612zM7.087 23.887c-.875 0-1.837-.35-2.538-1.05s-1.05-1.575-1.05-2.537.35-1.838 1.05-2.538c.7-.612 1.575-.962 2.538-.962s1.837.35 2.538 1.05c1.4 1.4 1.4 3.675 0 5.075-.7.612-1.575.962-2.538.962zm0-5.337c-.525 0-.962.175-1.313.525s-.525.788-.525 1.313.175.963.525 1.313a1.794 1.794 0 1 0 2.538-2.537c-.263-.438-.7-.612-1.225-.612z'/></svg>",
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; }
}
}

72
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<WebhookAction, WebhookJob>
{
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; }
}
}

33
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.1.0" />
<PackageReference Include="CoreTweet" Version="0.9.0.415" />
<PackageReference Include="Elasticsearch.Net" Version="6.3.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.1.1" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NodaTime" Version="2.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.2" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

25
libs/Dockerfile

@ -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

12
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<Language, LanguageConfig>.Empty, null, false);
public static readonly LanguagesConfig English = Build(Language.EN);
private readonly ImmutableDictionary<Language, LanguageConfig> 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 =

6
src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs

@ -25,8 +25,8 @@ namespace Squidex.Domain.Apps.Core.Contents
{
}
protected ContentData(IDictionary<T, ContentFieldData> copy, IEqualityComparer<T> comparer)
: base(copy, comparer)
protected ContentData(int capacity, IEqualityComparer<T> 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)
{

4
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<long>.Default)
public IdContentData(int capacity)
: base(capacity, EqualityComparer<long>.Default)
{
}

5
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<string>.Default)
public NamedContentData(int capacity)
: base(capacity, StringComparer.OrdinalIgnoreCase)
{
}

17
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
}
}

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

26
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AlgoliaAction.cs

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

24
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/AzureQueueAction.cs

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

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

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

24
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/FastlyAction.cs

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

25
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/SlackAction.cs

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

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

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

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

@ -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<out T>
{
T Visit(AlgoliaAction action);
T Visit(AzureQueueAction action);
T Visit(ElasticSearchAction action);
T Visit(FastlyAction action);
T Visit(SlackAction action);
T Visit(WebhookAction action);
}
}

2
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;

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

29
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<T>(IRuleActionVisitor<T> visitor);
public IEnumerable<ValidationError> Validate()
{
var context = new ValidationContext(this);
var errors = new List<ValidationResult>();
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<ValidationError> CustomValidate()
{
yield break;
}
}
}

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

16
src/Squidex.Domain.Apps.Core.Model/Rules/RuleJobData.cs

@ -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<string, JToken>
{
}
}

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

77
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<ArrayFieldProperties>, IArrayField
{
private FieldCollection<NestedField> fields = FieldCollection<NestedField>.Empty;
public IReadOnlyList<NestedField> Fields
{
get { return fields.Ordered; }
}
public IReadOnlyDictionary<long, NestedField> FieldsById
{
get { return fields.ById; }
}
public IReadOnlyDictionary<string, NestedField> 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<long> 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<NestedField, NestedField> updater)
{
return Updatefields(f => f.Update(fieldId, updater));
}
private ArrayField Updatefields(Func<FieldCollection<NestedField>, FieldCollection<NestedField>> updater)
{
var newFields = updater(fields);
if (ReferenceEquals(newFields, fields))
{
return this;
}
return Clone<ArrayField>(clone =>
{
clone.fields = newFields;
});
}
}
}

40
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<T>(IFieldPropertiesVisitor<T> visitor)
{
return visitor.Visit(this);
}
public override T Accept<T>(IFieldVisitor<T> 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();
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsField.cs

@ -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<AssetsFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldVisitor<T> visitor, IField field)
{
return new AssetsField(id, name, partitioning, this);
return visitor.Visit((IField<AssetsFieldProperties>)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);
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanField.cs

@ -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<BooleanFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldVisitor<T> visitor, IField field)
{
return new BooleanField(id, name, partitioning, this);
return visitor.Visit((IField<BooleanFieldProperties>)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);
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeField.cs

@ -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<DateTimeFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldVisitor<T> visitor, IField field)
{
return new DateTimeField(id, name, partitioning, this);
return visitor.Visit((IField<DateTimeFieldProperties>)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);
}
}
}

115
src/Squidex.Domain.Apps.Core.Model/Schemas/Field.cs

@ -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<Field>
{
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<Field>(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<T>(IFieldVisitor<T> visitor);
}
}

161
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<T> : Cloneable<FieldCollection<T>> where T : IField
{
public static readonly FieldCollection<T> Empty = new FieldCollection<T>();
private ImmutableArray<T> fieldsOrdered = ImmutableArray<T>.Empty;
private ImmutableDictionary<long, T> fieldsById;
private ImmutableDictionary<string, T> fieldsByName;
public IReadOnlyList<T> Ordered
{
get { return fieldsOrdered; }
}
public IReadOnlyDictionary<long, T> ById
{
get
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, T>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
}
public IReadOnlyDictionary<string, T> ByName
{
get
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, T>.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<T> Remove(long fieldId)
{
if (!ById.TryGetValue(fieldId, out var field))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.Remove(field);
});
}
[Pure]
public FieldCollection<T> Reorder(List<long> 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<T> 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<T> Update(long fieldId, Func<T, T> 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);
});
}
}
}

158
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<long> 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));
}
}
}

8
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<T>(IFieldPropertiesVisitor<T> visitor);
public abstract Field CreateField(long id, string name, Partitioning partitioning);
public abstract T Accept<T>(IFieldVisitor<T> visitor, IField field);
public abstract RootField CreateRootField(long id, string name, Partitioning partitioning);
public abstract NestedField CreateNestedField(long id, string name);
}
}

55
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<Type, FactoryFunction> fieldsByPropertyType = new Dictionary<Type, FactoryFunction>();
private readonly HashSet<Type> supportedFields = new HashSet<Type>();
public FieldRegistry(TypeNameRegistry typeNameRegistry)
{
@ -24,39 +23,47 @@ namespace Squidex.Domain.Apps.Core.Schemas
this.typeNameRegistry = typeNameRegistry;
RegisterField<AssetsFieldProperties>();
RegisterField<BooleanFieldProperties>();
RegisterField<DateTimeFieldProperties>();
RegisterField<GeolocationFieldProperties>();
RegisterField<JsonFieldProperties>();
RegisterField<NumberFieldProperties>();
RegisterField<ReferencesFieldProperties>();
RegisterField<StringFieldProperties>();
RegisterField<TagsFieldProperties>();
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<T>()
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);
}
}
}

226
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<ArrayFieldProperties> 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<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning, AssetsFieldProperties properties = null)
{
return new RootField<AssetsFieldProperties>(id, name, partitioning, properties ?? new AssetsFieldProperties());
}
public static RootField<BooleanFieldProperties> Boolean(long id, string name, Partitioning partitioning, BooleanFieldProperties properties = null)
{
return new RootField<BooleanFieldProperties>(id, name, partitioning, properties ?? new BooleanFieldProperties());
}
public static RootField<DateTimeFieldProperties> DateTime(long id, string name, Partitioning partitioning, DateTimeFieldProperties properties = null)
{
return new RootField<DateTimeFieldProperties>(id, name, partitioning, properties ?? new DateTimeFieldProperties());
}
public static RootField<GeolocationFieldProperties> Geolocation(long id, string name, Partitioning partitioning, GeolocationFieldProperties properties = null)
{
return new RootField<GeolocationFieldProperties>(id, name, partitioning, properties ?? new GeolocationFieldProperties());
}
public static RootField<JsonFieldProperties> Json(long id, string name, Partitioning partitioning, JsonFieldProperties properties = null)
{
return new RootField<JsonFieldProperties>(id, name, partitioning, properties ?? new JsonFieldProperties());
}
public static RootField<NumberFieldProperties> Number(long id, string name, Partitioning partitioning, NumberFieldProperties properties = null)
{
return new RootField<NumberFieldProperties>(id, name, partitioning, properties ?? new NumberFieldProperties());
}
public static RootField<ReferencesFieldProperties> References(long id, string name, Partitioning partitioning, ReferencesFieldProperties properties = null)
{
return new RootField<ReferencesFieldProperties>(id, name, partitioning, properties ?? new ReferencesFieldProperties());
}
public static RootField<StringFieldProperties> String(long id, string name, Partitioning partitioning, StringFieldProperties properties = null)
{
return new RootField<StringFieldProperties>(id, name, partitioning, properties ?? new StringFieldProperties());
}
public static RootField<TagsFieldProperties> Tags(long id, string name, Partitioning partitioning, TagsFieldProperties properties = null)
{
return new RootField<TagsFieldProperties>(id, name, partitioning, properties ?? new TagsFieldProperties());
}
public static NestedField<AssetsFieldProperties> Assets(long id, string name, AssetsFieldProperties properties = null)
{
return new NestedField<AssetsFieldProperties>(id, name, properties ?? new AssetsFieldProperties());
}
public static NestedField<BooleanFieldProperties> Boolean(long id, string name, BooleanFieldProperties properties = null)
{
return new NestedField<BooleanFieldProperties>(id, name, properties ?? new BooleanFieldProperties());
}
public static NestedField<DateTimeFieldProperties> DateTime(long id, string name, DateTimeFieldProperties properties = null)
{
return new NestedField<DateTimeFieldProperties>(id, name, properties ?? new DateTimeFieldProperties());
}
public static NestedField<GeolocationFieldProperties> Geolocation(long id, string name, GeolocationFieldProperties properties = null)
{
return new NestedField<GeolocationFieldProperties>(id, name, properties ?? new GeolocationFieldProperties());
}
public static NestedField<JsonFieldProperties> Json(long id, string name, JsonFieldProperties properties = null)
{
return new NestedField<JsonFieldProperties>(id, name, properties ?? new JsonFieldProperties());
}
public static NestedField<NumberFieldProperties> Number(long id, string name, NumberFieldProperties properties = null)
{
return new NestedField<NumberFieldProperties>(id, name, properties ?? new NumberFieldProperties());
}
public static NestedField<ReferencesFieldProperties> References(long id, string name, ReferencesFieldProperties properties = null)
{
return new NestedField<ReferencesFieldProperties>(id, name, properties ?? new ReferencesFieldProperties());
}
public static NestedField<StringFieldProperties> String(long id, string name, StringFieldProperties properties = null)
{
return new NestedField<StringFieldProperties>(id, name, properties ?? new StringFieldProperties());
}
public static NestedField<TagsFieldProperties> Tags(long id, string name, TagsFieldProperties properties = null)
{
return new NestedField<TagsFieldProperties>(id, name, properties ?? new TagsFieldProperties());
}
public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func<ArrayField, ArrayField> 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));
}
}
}

61
src/Squidex.Domain.Apps.Core.Model/Schemas/Field{T}.cs

@ -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<T> : 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<Field<T>>(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;
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/GeolocationField.cs

@ -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<GeolocationFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldVisitor<T> visitor, IField field)
{
return new GeolocationField(id, name, partitioning, this);
return visitor.Visit((IField<GeolocationFieldProperties>)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);
}
}
}

20
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<ArrayFieldProperties>
{
IReadOnlyList<NestedField> Fields { get; }
IReadOnlyDictionary<long, NestedField> FieldsById { get; }
IReadOnlyDictionary<string, NestedField> FieldsByName { get; }
}
}

26
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<T>(IFieldVisitor<T> visitor);
}
}

2
src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs

@ -9,6 +9,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldPropertiesVisitor<out T>
{
T Visit(ArrayFieldProperties properties);
T Visit(AssetsFieldProperties properties);
T Visit(BooleanFieldProperties properties);

20
src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs

@ -9,22 +9,24 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public interface IFieldVisitor<out T>
{
T Visit(AssetsField field);
T Visit(IArrayField field);
T Visit(BooleanField field);
T Visit(IField<AssetsFieldProperties> field);
T Visit(DateTimeField field);
T Visit(IField<BooleanFieldProperties> field);
T Visit(GeolocationField field);
T Visit(IField<DateTimeFieldProperties> field);
T Visit(JsonField field);
T Visit(IField<GeolocationFieldProperties> field);
T Visit(NumberField field);
T Visit(IField<JsonFieldProperties> field);
T Visit(ReferencesField field);
T Visit(IField<NumberFieldProperties> field);
T Visit(StringField field);
T Visit(IField<ReferencesFieldProperties> field);
T Visit(TagsField field);
T Visit(IField<StringFieldProperties> field);
T Visit(IField<TagsFieldProperties> field);
}
}

14
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<out T> : IField
{
T Properties { get; }
}
}

13
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
{
}
}

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

14
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<JsonNestedFieldModel> Children { get; set; }
}
}

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

53
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<JsonNestedFieldModel> 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)
{

27
src/Squidex.Domain.Apps.Core.Model/Schemas/JsonField.cs

@ -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<JsonFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldPropertiesVisitor<T> 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<T>(IFieldVisitor<T> visitor, IField field)
{
return new JsonField(id, name, partitioning, this);
return visitor.Visit((IField<JsonFieldProperties>)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);
}
}
}

106
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<NestedField>, 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<T>(IFieldVisitor<T> visitor);
public abstract NestedField Update(FieldProperties newProperties);
}
}

70
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<T> : NestedField, IField<T> 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<NestedField<T>>(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<TResult>(IFieldVisitor<TResult> visitor)
{
return properties.Accept(visitor, this);
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/NumberField.cs

@ -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<NumberFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<double> 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<T>(IFieldVisitor<T> visitor, IField field)
{
return new NumberField(id, name, partitioning, this);
return visitor.Visit((IField<NumberFieldProperties>)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);
}
}
}

27
src/Squidex.Domain.Apps.Core.Model/Schemas/ReferencesField.cs

@ -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<ReferencesFieldProperties>
{
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<T>(IFieldVisitor<T> visitor)
{
return visitor.Visit(this);
}
}
}

16
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<T>(IFieldVisitor<T> visitor, IField field)
{
return new ReferencesField(id, name, partitioning, this);
return visitor.Visit((IField<ReferencesFieldProperties>)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);
}
}
}

115
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<RootField>, 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<T>(IFieldVisitor<T> visitor);
public abstract RootField Update(FieldProperties newProperties);
}
}

70
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<T> : RootField, IField<T> 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<RootField<T>>(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<TResult>(IFieldVisitor<TResult> visitor)
{
return properties.Accept(visitor, this);
}
}
}

164
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<Schema>
{
private readonly string name;
private ImmutableArray<Field> fieldsOrdered = ImmutableArray<Field>.Empty;
private ImmutableDictionary<long, Field> fieldsById;
private ImmutableDictionary<string, Field> fieldsByName;
private FieldCollection<RootField> fields = FieldCollection<RootField>.Empty;
private SchemaProperties properties;
private bool isPublished;
@ -33,49 +29,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
get { return isPublished; }
}
public IReadOnlyList<Field> Fields
public IReadOnlyList<RootField> Fields
{
get { return fieldsOrdered; }
get { return fields.Ordered; }
}
public IReadOnlyDictionary<long, Field> FieldsById
public IReadOnlyDictionary<long, RootField> FieldsById
{
get
{
if (fieldsById == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsById = ImmutableDictionary<long, Field>.Empty;
}
else
{
fieldsById = fieldsOrdered.ToImmutableDictionary(x => x.Id);
}
}
return fieldsById;
}
get { return fields.ById; }
}
public IReadOnlyDictionary<string, Field> FieldsByName
public IReadOnlyDictionary<string, RootField> FieldsByName
{
get
{
if (fieldsByName == null)
{
if (fieldsOrdered.Length == 0)
{
fieldsByName = ImmutableDictionary<string, Field>.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<RootField>(fields);
protected override void OnCloned()
{
fieldsById = null;
fieldsByName = null;
this.isPublished = isPublished;
}
[Pure]
@ -122,60 +81,6 @@ namespace Squidex.Domain.Apps.Core.Schemas
});
}
[Pure]
public Schema UpdateField(long fieldId, FieldProperties newProperties)
{
return UpdateField(fieldId, field =>
{
return field.Update(newProperties);
});
}
[Pure]
public Schema LockField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Lock();
});
}
[Pure]
public Schema DisableField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Disable();
});
}
[Pure]
public Schema EnableField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Enable();
});
}
[Pure]
public Schema HideField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Hide();
});
}
[Pure]
public Schema ShowField(long fieldId)
{
return UpdateField(fieldId, field =>
{
return field.Show();
});
}
[Pure]
public Schema Publish()
{
@ -197,62 +102,39 @@ namespace Squidex.Domain.Apps.Core.Schemas
[Pure]
public Schema DeleteField(long fieldId)
{
if (!FieldsById.TryGetValue(fieldId, out var field))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = fieldsOrdered.Remove(field);
});
return Updatefields(f => f.Remove(fieldId));
}
[Pure]
public Schema ReorderFields(List<long> ids)
{
Guard.NotNull(ids, nameof(ids));
if (ids.Count != fieldsOrdered.Length || ids.Any(x => !FieldsById.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();
});
return Updatefields(f => f.Reorder(ids));
}
[Pure]
public Schema AddField(Field field)
public Schema AddField(RootField field)
{
Guard.NotNull(field, nameof(field));
if (FieldsByName.ContainsKey(field.Name) || FieldsById.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);
});
return Updatefields(f => f.Add(field));
}
[Pure]
public Schema UpdateField(long fieldId, Func<Field, Field> updater)
public Schema UpdateField(long fieldId, Func<RootField, RootField> updater)
{
return Updatefields(f => f.Update(fieldId, updater));
}
private Schema Updatefields(Func<FieldCollection<RootField>, FieldCollection<RootField>> updater)
{
Guard.NotNull(updater, nameof(updater));
var newFields = updater(fields);
if (!FieldsById.TryGetValue(fieldId, out var field))
if (ReferenceEquals(newFields, fields))
{
return this;
}
return Clone(clone =>
{
clone.fieldsOrdered = clone.fieldsOrdered.Replace(field, updater(field));
clone.fields = newFields;
});
}
}

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

Loading…
Cancel
Save