Browse Source

Feature/netcore3.0 (#433)

* Migration to .NET Core 3.0
* Migration to Orleans 3.0
* Nullable support
* Better separation of frontend and backend.
pull/443/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
835274396f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      .dockerignore
  2. 16
      .drone.yml
  3. 4
      .gitignore
  4. 66
      Dockerfile
  5. 37
      Dockerfile.build
  6. 16
      backend/.editorconfig
  7. 0
      backend/NuGet.Config
  8. 0
      backend/Squidex.ruleset
  9. 0
      backend/Squidex.sln
  10. 0
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs
  11. 135
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs
  12. 0
      backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs
  13. 0
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs
  14. 0
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs
  15. 0
      backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs
  16. 0
      backend/extensions/Squidex.Extensions/Actions/ClientPool.cs
  17. 0
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs
  18. 0
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs
  19. 0
      backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs
  20. 0
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs
  21. 0
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs
  22. 0
      backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs
  23. 0
      backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs
  24. 0
      backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs
  25. 0
      backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs
  26. 0
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs
  27. 70
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.cs
  28. 0
      backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs
  29. 0
      backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs
  30. 0
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs
  31. 0
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs
  32. 0
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs
  33. 0
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs
  34. 0
      backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs
  35. 0
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs
  36. 0
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs
  37. 0
      backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs
  38. 0
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs
  39. 0
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs
  40. 0
      backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs
  41. 0
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs
  42. 68
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.cs
  43. 0
      backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs
  44. 0
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs
  45. 72
      backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs
  46. 0
      backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs
  47. 0
      backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs
  48. 0
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs
  49. 86
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs
  50. 0
      backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs
  51. 0
      backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs
  52. 0
      backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs
  53. 31
      backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj
  54. 46
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  55. 90
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs
  56. 45
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs
  57. 34
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs
  58. 35
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs
  59. 62
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs
  60. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs
  61. 40
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs
  62. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs
  63. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs
  64. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs
  65. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs
  66. 38
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs
  67. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs
  68. 62
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs
  69. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs
  70. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs
  71. 62
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  72. 179
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs
  73. 76
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  74. 180
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  75. 38
      backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  76. 99
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs
  77. 70
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  78. 55
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs
  79. 65
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs
  80. 54
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs
  81. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs
  82. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs
  83. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs
  84. 54
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  85. 62
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs
  86. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs
  87. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs
  88. 36
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs
  89. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
  90. 126
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  91. 31
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  92. 27
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
  93. 83
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs
  94. 0
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml
  95. 0
      backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd
  96. 0
      backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs
  97. 0
      backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs
  98. 0
      backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs
  99. 74
      backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs
  100. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Named.cs

16
.dockerignore

@ -8,19 +8,19 @@
.git
# Build results
bin/
build/
obj/
out/
publish/
**/bin
**/build
**/obj
**/out
**/publish
# Test Output
_test-output/
**/_test-output
# NodeJS
node_modules/
**/node_modules
src/Squidex/Assets/*.*
backend/src/Squidex/Assets/*.*
**/appsettings.Development.json
**/appsettings.Production.json

16
.drone.yml

@ -63,9 +63,9 @@ steps:
- name: build_binaries
image: docker
commands:
- docker build -t squidex-build-image -f Dockerfile.build --build-arg SQUIDEX__VERSION=$${DRONE_TAG} .
- docker build -t squidex-build-image -f Dockerfile --build-arg SQUIDEX__VERSION=$${DRONE_TAG} .
- docker create --name squidex-build-container squidex-build-image
- docker cp squidex-build-container:/out /build
- docker cp squidex-build-container:/app /build
volumes:
- name: build
path: /build
@ -117,8 +117,10 @@ steps:
- name: cleanup-build
image: docker
commands:
- docker rm squidex-build-container
- docker rmi squidex-build-image
- docker rm squidex-backend-container
- docker rm squidex-frontend-container
- docker rmi squidex-backend-image
- docker rmi squidex-frontend-image
volumes:
- name: docker1
path: /var/run/docker.sock
@ -141,8 +143,10 @@ steps:
- name: docker2
path: /var/lib/docker
when:
status:
- failure
event:
- push
branch:
- cleaned
volumes:
- name: build

4
.gitignore

@ -13,6 +13,7 @@
bin/
build/
obj/
out/
publish/
# Test Output
@ -21,9 +22,6 @@ _test-output/
# NodeJS
node_modules/
# Scripts (should be copied from node_modules on build)
**/wwwroot/scripts/**/*.*
/src/Squidex/Assets/
appsettings.Development.json

66
Dockerfile

@ -1,33 +1,21 @@
#
# Stage 1, Prebuild
# Stage 1, Build Backend
#
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
FROM mcr.microsoft.com/dotnet/core/sdk:3.0-buster as backend
ARG SQUIDEX__VERSION=1.0.0
WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/
# Install Node packages
RUN cd /tmp && npm install --loglevel=error
# Copy nuget project files.
COPY /**/**/*.csproj /tmp/
COPY backend/**/**/*.csproj /tmp/
# Copy nuget.config for package sources.
COPY NuGet.Config /tmp/
COPY backend/NuGet.Config /tmp/
# Install nuget packages
RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd'
COPY . .
# Build Frontend
RUN cp -a /tmp/node_modules src/Squidex/ \
&& cd src/Squidex \
&& npm run test:coverage \
&& npm run build
COPY backend .
# Test Backend
RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
@ -37,27 +25,45 @@ RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.
&& dotnet test tests/Squidex.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/alpine --configuration Release -r alpine.3.7-x64 -p:version=$SQUIDEX__VERSION
RUN dotnet publish src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION
#
# Stage 2, Build Frontend
#
FROM buildkite/puppeteer:latest as frontend
WORKDIR /src
# Copy Node project files.
COPY frontend/package*.json /tmp/
# Install Node packages
RUN cd /tmp && npm install --loglevel=error
COPY frontend .
# Build Frontend
RUN cp -a /tmp/node_modules . \
&& npm run test:coverage \
&& npm run build
RUN cp -a build /build/
#
# Stage 2, Build runtime
# Stage 3, Build runtime
#
FROM mcr.microsoft.com/dotnet/core/runtime-deps:2.2-alpine3.8
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-buster-slim
# Default AspNetCore directory
WORKDIR /app
# add libuv & curl
RUN apk update \
&& apk add --no-cache libc6-compat \
&& apk add --no-cache libuv \
&& apk add --no-cache curl \
&& ln -s /usr/lib/libuv.so.1 /usr/lib/libuv.so
# Copy from build stage
COPY --from=builder /out/alpine .
# Copy from build stages
COPY --from=backend /build/ .
COPY --from=frontend /build/ wwwroot/build/
EXPOSE 80
EXPOSE 11111
ENTRYPOINT ["./Squidex"]
ENTRYPOINT ["dotnet", "Squidex.dll"]

37
Dockerfile.build

@ -1,37 +0,0 @@
FROM squidex/dotnet:2.2-sdk-chromium-phantomjs-node as builder
ARG SQUIDEX__VERSION=1.0.0
WORKDIR /src
# Copy Node project files.
COPY src/Squidex/package*.json /tmp/
# Install Node packages
RUN cd /tmp && npm install --loglevel=error
# Copy Dotnet project files.
COPY /**/**/*.csproj /tmp/
# Copy nuget.config for package sources.
COPY NuGet.Config /tmp/
# Install Dotnet packages
RUN bash -c 'pushd /tmp; for p in *.csproj; do dotnet restore $p --verbosity quiet; true; done; popd'
COPY . .
# Build Frontend
RUN cp -a /tmp/node_modules src/Squidex/ \
&& cd src/Squidex \
&& npm run test:coverage \
&& npm run build
# Test Backend
RUN dotnet test tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj --filter Category!=Dependencies \
&& 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.Web.Tests/Squidex.Web.Tests.csproj
# Publish
RUN dotnet publish src/Squidex/Squidex.csproj --output /out/ --configuration Release -p:version=$SQUIDEX__VERSION

16
backend/.editorconfig

@ -0,0 +1,16 @@
[*.cs]
# CS8618: Non-nullable field is uninitialized. Consider declaring as nullable.
dotnet_diagnostic.CS8618.severity = none
# SA1011: Closing square brackets should be spaced correctly
dotnet_diagnostic.SA1011.severity = none
# IDE0066: Convert switch statement to expression
csharp_style_prefer_switch_expression = false:suggestion
# IDE0010: Add missing cases
dotnet_diagnostic.IDE0010.severity = none
# IDE0063: Use simple 'using' statement
csharp_prefer_simple_using_statement = false:suggestion

0
NuGet.Config → backend/NuGet.Config

0
Squidex.ruleset → backend/Squidex.ruleset

0
Squidex.sln → backend/Squidex.sln

0
extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs → backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaAction.cs

135
backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaActionHandler.cs

@ -0,0 +1,135 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
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;
using AlgoliaIndex = Algolia.Search.Index;
#pragma warning disable IDE0059 // Value assigned to symbol is never used
namespace Squidex.Extensions.Actions.Algolia
{
public sealed class AlgoliaActionHandler : RuleActionHandler<AlgoliaAction, AlgoliaJob>
{
private readonly ClientPool<(string AppId, string ApiKey, string IndexName), AlgoliaIndex> clients;
public AlgoliaActionHandler(RuleEventFormatter formatter)
: base(formatter)
{
clients = new ClientPool<(string AppId, string ApiKey, string IndexName), AlgoliaIndex>(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}";
JObject json;
try
{
string jsonString;
if (!string.IsNullOrEmpty(action.Document))
{
jsonString = Format(action.Document, @event)?.Trim();
}
else
{
jsonString = ToJson(contentEvent);
}
json = JObject.Parse(jsonString);
}
catch (Exception ex)
{
json = new JObject(new JProperty("error", $"Invalid JSON: {ex.Message}"));
}
ruleJob.Content = json;
ruleJob.Content["objectID"] = contentId;
}
return (ruleDescription, ruleJob);
}
return ("Ignore", new AlgoliaJob());
}
protected override async Task<Result> ExecuteJobAsync(AlgoliaJob job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.AppId))
{
return Result.Ignored();
}
var index = clients.GetClient((job.AppId, job.ApiKey, job.IndexName));
try
{
if (job.Content != null)
{
var response = await index.PartialUpdateObjectAsync(job.Content, true, ct);
return Result.Success(response.ToString(Formatting.Indented));
}
else
{
var response = await index.DeleteObjectAsync(job.ContentId, ct);
return Result.Success(response.ToString(Formatting.Indented));
}
}
catch (AlgoliaException ex)
{
return Result.Failed(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; }
}
}

0
extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Algolia/AlgoliaPlugin.cs

0
extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs → backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueAction.cs

0
extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueueActionHandler.cs

0
extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs → backend/extensions/Squidex.Extensions/Actions/AzureQueue/AzureQueuePlugin.cs

0
extensions/Squidex.Extensions/Actions/ClientPool.cs → backend/extensions/Squidex.Extensions/Actions/ClientPool.cs

0
extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs → backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseAction.cs

0
extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/Discourse/DiscourseActionHandler.cs

0
extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs → backend/extensions/Squidex.Extensions/Actions/Discourse/DiscoursePlugin.cs

0
extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs → backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchAction.cs

0
extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchActionHandler.cs

0
extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs → backend/extensions/Squidex.Extensions/Actions/ElasticSearch/ElasticSearchPlugin.cs

0
extensions/Squidex.Extensions/Actions/Email/EmailAction.cs → backend/extensions/Squidex.Extensions/Actions/Email/EmailAction.cs

0
extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/Email/EmailActionHandler.cs

0
extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Email/EmailPlugin.cs

0
extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs → backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyAction.cs

70
backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyActionHandler.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.Net.Http;
using System.Threading;
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);
this.httpClientFactory = httpClientFactory;
}
protected override (string Description, FastlyJob Data) CreateJob(EnrichedEvent @event, FastlyAction action)
{
var id = @event is IEnrichedEntityEvent entityEvent ? entityEvent.Id.ToString() : string.Empty;
var ruleJob = new FastlyJob
{
Key = id,
FastlyApiKey = action.ApiKey,
FastlyServiceID = action.ServiceId
};
return (Description, ruleJob);
}
protected override async Task<Result> ExecuteJobAsync(FastlyJob job, CancellationToken ct = default)
{
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, ct: ct);
}
}
}
public sealed class FastlyJob
{
public string FastlyApiKey { get; set; }
public string FastlyServiceID { get; set; }
public string Key { get; set; }
}
}

0
extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Fastly/FastlyPlugin.cs

0
extensions/Squidex.Extensions/Actions/HttpHelper.cs → backend/extensions/Squidex.Extensions/Actions/HttpHelper.cs

0
extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs → backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaAction.cs

0
extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaActionHandler.cs

0
extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaPlugin.cs

0
extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs → backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducer.cs

0
extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs → backend/extensions/Squidex.Extensions/Actions/Kafka/KafkaProducerOptions.cs

0
extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs → backend/extensions/Squidex.Extensions/Actions/Medium/MediumAction.cs

0
extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/Medium/MediumActionHandler.cs

0
extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Medium/MediumPlugin.cs

0
extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs → backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderAction.cs

0
extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs → backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderActionHandler.cs

0
extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Prerender/PrerenderPlugin.cs

0
extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs → backend/extensions/Squidex.Extensions/Actions/Slack/SlackAction.cs

68
backend/extensions/Squidex.Extensions/Actions/Slack/SlackActionHandler.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.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
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);
this.httpClientFactory = httpClientFactory;
}
protected override (string Description, SlackJob Data) CreateJob(EnrichedEvent @event, SlackAction action)
{
var body = new { text = Format(action.Text, @event) };
var ruleJob = new SlackJob
{
RequestUrl = action.WebhookUrl.ToString(),
RequestBody = ToJson(body)
};
return (Description, ruleJob);
}
protected override async Task<Result> ExecuteJobAsync(SlackJob job, CancellationToken ct = default)
{
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, ct);
}
}
}
public sealed class SlackJob
{
public string RequestUrl { get; set; }
public string RequestBody { get; set; }
}
}

0
extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Slack/SlackPlugin.cs

0
extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs → backend/extensions/Squidex.Extensions/Actions/Twitter/TweetAction.cs

72
backend/extensions/Squidex.Extensions/Actions/Twitter/TweetActionHandler.cs

@ -0,0 +1,72 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading;
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);
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<Result> ExecuteJobAsync(TweetJob job, CancellationToken ct = default)
{
var tokens = Tokens.Create(
twitterOptions.ClientId,
twitterOptions.ClientSecret,
job.AccessToken,
job.AccessSecret);
var request = new Dictionary<string, object>
{
["status"] = job.Text
};
await tokens.Statuses.UpdateAsync(request, ct);
return Result.Success($"Tweeted: {job.Text}");
}
}
public sealed class TweetJob
{
public string AccessToken { get; set; }
public string AccessSecret { get; set; }
public string Text { get; set; }
}
}

0
extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs → backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterOptions.cs

0
extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Twitter/TwitterPlugin.cs

0
extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs → backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookAction.cs

86
backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookActionHandler.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Net.Http;
using System.Text;
using System.Threading;
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 static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(2);
private readonly IHttpClientFactory httpClientFactory;
public WebhookActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory)
: base(formatter)
{
Guard.NotNull(httpClientFactory);
this.httpClientFactory = httpClientFactory;
}
protected override (string Description, WebhookJob Data) CreateJob(EnrichedEvent @event, WebhookAction action)
{
string requestBody;
if (!string.IsNullOrEmpty(action.Payload))
{
requestBody = Format(action.Payload, @event);
}
else
{
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<Result> ExecuteJobAsync(WebhookJob job, CancellationToken ct = default)
{
using (var httpClient = httpClientFactory.CreateClient())
{
httpClient.Timeout = DefaultTimeout;
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, ct);
}
}
}
public sealed class WebhookJob
{
public string RequestUrl { get; set; }
public string RequestSignature { get; set; }
public string RequestBody { get; set; }
}
}

0
extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs → backend/extensions/Squidex.Extensions/Actions/Webhook/WebhookPlugin.cs

0
extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs → backend/extensions/Squidex.Extensions/Samples/AssetStore/MemoryAssetStorePlugin.cs

0
extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs → backend/extensions/Squidex.Extensions/Samples/Controllers/PluginController.cs

31
backend/extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Domain.Apps.Core.Operations\Squidex.Domain.Apps.Core.Operations.csproj" />
<ProjectReference Include="..\..\src\Squidex.Web\Squidex.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="Confluent.Kafka" Version="1.2.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.8.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.6.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="NodaTime" Version="2.4.7" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="WindowsAzure.Storage" Version="9.3.3" />
</ItemGroup>
<PropertyGroup>
<CodeAnalysisRuleSet>..\..\Squidex.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
</Project>

46
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -0,0 +1,46 @@
// ==========================================================================
// 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.Apps
{
public sealed class AppClient : Named
{
public string Role { get; }
public string Secret { get; }
public AppClient(string name, string secret, string role)
: base(name)
{
Guard.NotNullOrEmpty(secret);
Guard.NotNullOrEmpty(role);
Role = role;
Secret = secret;
}
[Pure]
public AppClient Update(string newRole)
{
Guard.NotNullOrEmpty(newRole);
return new AppClient(Name, Secret, newRole);
}
[Pure]
public AppClient Rename(string newName)
{
Guard.NotNullOrEmpty(newName);
return new AppClient(newName, Secret, Role);
}
}
}

90
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs

@ -0,0 +1,90 @@
// ==========================================================================
// 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.Diagnostics.Contracts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppClients : ArrayDictionary<string, AppClient>
{
public static readonly AppClients Empty = new AppClients();
private AppClients()
{
}
public AppClients(KeyValuePair<string, AppClient>[] items)
: base(items)
{
}
[Pure]
public AppClients Revoke(string id)
{
Guard.NotNullOrEmpty(id);
return new AppClients(Without(id));
}
[Pure]
public AppClients Add(string id, AppClient client)
{
Guard.NotNullOrEmpty(id);
Guard.NotNull(client);
if (ContainsKey(id))
{
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppClients(With(id, client));
}
[Pure]
public AppClients Add(string id, string secret)
{
Guard.NotNullOrEmpty(id);
if (ContainsKey(id))
{
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppClients(With(id, new AppClient(id, secret, Role.Editor)));
}
[Pure]
public AppClients Rename(string id, string newName)
{
Guard.NotNullOrEmpty(id);
if (!TryGetValue(id, out var client))
{
return this;
}
return new AppClients(With(id, client.Rename(newName)));
}
[Pure]
public AppClients Update(string id, string role)
{
Guard.NotNullOrEmpty(id);
if (!TryGetValue(id, out var client))
{
return this;
}
return new AppClients(With(id, client.Update(role)));
}
}
}

45
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppContributors.cs

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppContributors : ArrayDictionary<string, string>
{
public static readonly AppContributors Empty = new AppContributors();
private AppContributors()
{
}
public AppContributors(KeyValuePair<string, string>[] items)
: base(items)
{
}
[Pure]
public AppContributors Assign(string contributorId, string role)
{
Guard.NotNullOrEmpty(contributorId);
Guard.NotNullOrEmpty(role);
return new AppContributors(With(contributorId, role));
}
[Pure]
public AppContributors Remove(string contributorId)
{
Guard.NotNullOrEmpty(contributorId);
return new AppContributors(Without(contributorId));
}
}
}

34
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs

@ -0,0 +1,34 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppImage
{
public string MimeType { get; }
public string Etag { get; }
public AppImage(string mimeType, string? etag = null)
{
Guard.NotNullOrEmpty(mimeType);
MimeType = mimeType;
if (string.IsNullOrWhiteSpace(etag))
{
Etag = RandomHash.Simple();
}
else
{
Etag = etag;
}
}
}
}

35
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs

@ -0,0 +1,35 @@
// ==========================================================================
// 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.Apps
{
public sealed class AppPattern : Named
{
public string Pattern { get; }
public string? Message { get; }
public AppPattern(string name, string pattern, string? message = null)
: base(name)
{
Guard.NotNullOrEmpty(pattern);
Pattern = pattern;
Message = message;
}
[Pure]
public AppPattern Update(string newName, string newPattern, string? newMessage)
{
return new AppPattern(newName, newPattern, newMessage);
}
}
}

62
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs

@ -0,0 +1,62 @@
// ==========================================================================
// 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.Diagnostics.Contracts;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPatterns : ArrayDictionary<Guid, AppPattern>
{
public static readonly AppPatterns Empty = new AppPatterns();
private AppPatterns()
{
}
public AppPatterns(KeyValuePair<Guid, AppPattern>[] items)
: base(items)
{
}
[Pure]
public AppPatterns Remove(Guid id)
{
return new AppPatterns(Without(id));
}
[Pure]
public AppPatterns Add(Guid id, string name, string pattern, string? message)
{
var newPattern = new AppPattern(name, pattern, message);
if (ContainsKey(id))
{
throw new ArgumentException("Id already exists.", nameof(id));
}
return new AppPatterns(With(id, newPattern));
}
[Pure]
public AppPatterns Update(Guid id, string name, string pattern, string? message)
{
Guard.NotNullOrEmpty(name);
Guard.NotNullOrEmpty(pattern);
if (!TryGetValue(id, out var appPattern))
{
return this;
}
return new AppPatterns(With(id, appPattern.Update(name, pattern, message)));
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPermission.cs

40
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs

@ -0,0 +1,40 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class AppPlan
{
public RefToken Owner { get; }
public string PlanId { get; }
public AppPlan(RefToken owner, string planId)
{
Guard.NotNull(owner);
Guard.NotNullOrEmpty(planId);
Owner = owner;
PlanId = planId;
}
public static AppPlan? Build(RefToken owner, string planId)
{
if (planId == null)
{
return null;
}
else
{
return new AppPlan(owner, planId);
}
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppContributorsConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs

38
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public class JsonAppPattern
{
[JsonProperty]
public string Name { get; set; }
[JsonProperty]
public string Pattern { get; set; }
[JsonProperty]
public string? Message { get; set; }
public JsonAppPattern()
{
}
public JsonAppPattern(AppPattern pattern)
{
SimpleMapper.Map(pattern, this);
}
public AppPattern ToPattern()
{
return new AppPattern(Name, Pattern, Message);
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguageConfig.cs

62
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonLanguagesConfig.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Newtonsoft.Json;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps.Json
{
public sealed class JsonLanguagesConfig
{
[JsonProperty]
public Dictionary<string, JsonLanguageConfig> Languages { get; set; }
[JsonProperty]
public Language? Master { get; set; }
public JsonLanguagesConfig()
{
}
public JsonLanguagesConfig(LanguagesConfig value)
{
Languages = new Dictionary<string, JsonLanguageConfig>(value.Count);
foreach (LanguageConfig config in value)
{
Languages.Add(config.Language, new JsonLanguageConfig(config));
}
Master = value.Master?.Language;
}
public LanguagesConfig ToConfig()
{
var languagesConfig = new LanguageConfig[Languages?.Count ?? 0];
if (Languages != null)
{
var i = 0;
foreach (var config in Languages)
{
languagesConfig[i++] = config.Value.ToConfig(config.Key);
}
}
var result = LanguagesConfig.Build(languagesConfig);
if (Master != null)
{
result = result.MakeMaster(Master);
}
return result;
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/LanguagesConfigConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/RolesConverter.cs

62
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -0,0 +1,62 @@
// ==========================================================================
// 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.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguageConfig : IFieldPartitionItem
{
private readonly Language language;
private readonly Language[] languageFallbacks;
public bool IsOptional { get; }
public Language Language
{
get { return language; }
}
public IEnumerable<Language> LanguageFallbacks
{
get { return languageFallbacks; }
}
string IFieldPartitionItem.Key
{
get { return language.Iso2Code; }
}
string IFieldPartitionItem.Name
{
get { return language.EnglishName; }
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return LanguageFallbacks.Select(x => x.Iso2Code); }
}
public LanguageConfig(Language language, bool isOptional = false, IEnumerable<Language>? fallback = null)
: this(language, isOptional, fallback?.ToArray())
{
}
public LanguageConfig(Language language, bool isOptional = false, params Language[]? fallback)
{
Guard.NotNull(language);
IsOptional = isOptional;
this.language = language;
this.languageFallbacks = fallback ?? Array.Empty<Language>();
}
}
}

179
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguagesConfig.cs

@ -0,0 +1,179 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class LanguagesConfig : IFieldPartitioning
{
public static readonly LanguagesConfig English = Build(Language.EN);
private readonly ArrayDictionary<Language, LanguageConfig> languages;
private readonly LanguageConfig master;
public LanguageConfig Master
{
get { return master; }
}
IFieldPartitionItem IFieldPartitioning.Master
{
get { return master; }
}
IEnumerator IEnumerable.GetEnumerator()
{
return languages.Values.GetEnumerator();
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
{
return languages.Values.GetEnumerator();
}
public int Count
{
get { return languages.Count; }
}
private LanguagesConfig(ArrayDictionary<Language, LanguageConfig> languages, LanguageConfig master, bool checkMaster = true)
{
if (checkMaster)
{
this.master = master ?? throw new InvalidOperationException("Config has no master language.");
}
foreach (var languageConfig in languages.Values)
{
foreach (var fallback in languageConfig.LanguageFallbacks)
{
if (!languages.ContainsKey(fallback))
{
var message = $"Config for language '{languageConfig.Language.Iso2Code}' contains unsupported fallback language '{fallback.Iso2Code}'";
throw new InvalidOperationException(message);
}
}
}
this.languages = languages;
}
public static LanguagesConfig Build(ICollection<LanguageConfig> configs)
{
Guard.NotNull(configs);
return new LanguagesConfig(configs.ToArrayDictionary(x => x.Language), configs.FirstOrDefault());
}
public static LanguagesConfig Build(params LanguageConfig[] configs)
{
return Build(configs?.ToList()!);
}
public static LanguagesConfig Build(params Language[] languages)
{
return Build(languages?.Select(x => new LanguageConfig(x)).ToList()!);
}
[Pure]
public LanguagesConfig MakeMaster(Language language)
{
Guard.NotNull(language);
return new LanguagesConfig(languages, languages[language]);
}
[Pure]
public LanguagesConfig Set(Language language, bool isOptional = false, IEnumerable<Language>? fallback = null)
{
Guard.NotNull(language);
return Set(new LanguageConfig(language, isOptional, fallback));
}
[Pure]
public LanguagesConfig Set(LanguageConfig config)
{
Guard.NotNull(config);
var newLanguages =
new ArrayDictionary<Language, LanguageConfig>(languages.With(config.Language, config));
var newMaster = Master?.Language == config.Language ? config : Master;
return new LanguagesConfig(newLanguages, newMaster!);
}
[Pure]
public LanguagesConfig Remove(Language language)
{
Guard.NotNull(language);
var newLanguages =
languages.Values.Where(x => x.Language != language)
.Select(config => new LanguageConfig(
config.Language,
config.IsOptional,
config.LanguageFallbacks.Except(new[] { language })))
.ToArrayDictionary(x => x.Language);
var newMaster =
newLanguages.Values.FirstOrDefault(x => x.Language == Master.Language) ??
newLanguages.Values.FirstOrDefault();
return new LanguagesConfig(newLanguages, newMaster);
}
public bool Contains(Language language)
{
return language != null && languages.ContainsKey(language);
}
public bool TryGetConfig(Language language, [MaybeNullWhen(false)] out LanguageConfig config)
{
return languages.TryGetValue(language, out config!);
}
public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item)
{
if (Language.IsValidLanguage(key) && languages.TryGetValue(key, out var value))
{
item = value;
return true;
}
else
{
item = null!;
return false;
}
}
public PartitionResolver ToResolver()
{
return partitioning =>
{
if (partitioning.Equals(Partitioning.Invariant))
{
return InvariantPartitioning.Instance;
}
return this;
};
}
}
}

76
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -0,0 +1,76 @@
// ==========================================================================
// 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.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Security;
using AllPermissions = Squidex.Shared.Permissions;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Role : Named
{
public const string Editor = "Editor";
public const string Developer = "Developer";
public const string Owner = "Owner";
public const string Reader = "Reader";
public PermissionSet Permissions { get; }
public bool IsDefault
{
get { return Roles.IsDefault(this); }
}
public Role(string name, PermissionSet permissions)
: base(name)
{
Guard.NotNull(permissions);
Permissions = permissions;
}
public Role(string name, params string[] permissions)
: this(name, new PermissionSet(permissions))
{
}
[Pure]
public Role Update(string[] permissions)
{
return new Role(Name, new PermissionSet(permissions));
}
public bool Equals(string name)
{
return name != null && name.Equals(Name, StringComparison.Ordinal);
}
public Role ForApp(string app)
{
var result = new HashSet<Permission>
{
AllPermissions.ForApp(AllPermissions.AppCommon, app)
};
if (Permissions.Any())
{
var prefix = AllPermissions.ForApp(AllPermissions.App, app).Id;
foreach (var permission in Permissions)
{
result.Add(new Permission(string.Concat(prefix, ".", permission.Id)));
}
}
return new Role(Name, new PermissionSet(result));
}
}
}

180
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -0,0 +1,180 @@
// ==========================================================================
// 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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Core.Apps
{
public sealed class Roles
{
private readonly ArrayDictionary<string, Role> inner;
public static readonly IReadOnlyDictionary<string, Role> Defaults = new Dictionary<string, Role>
{
[Role.Owner] =
new Role(Role.Owner, new PermissionSet(
Clean(Permissions.App))),
[Role.Reader] =
new Role(Role.Reader, new PermissionSet(
Clean(Permissions.AppAssetsRead),
Clean(Permissions.AppContentsRead))),
[Role.Editor] =
new Role(Role.Editor, new PermissionSet(
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppWorkflowsRead))),
[Role.Developer] =
new Role(Role.Developer, new PermissionSet(
Clean(Permissions.AppApi),
Clean(Permissions.AppAssets),
Clean(Permissions.AppContents),
Clean(Permissions.AppPatterns),
Clean(Permissions.AppRolesRead),
Clean(Permissions.AppRules),
Clean(Permissions.AppSchemas),
Clean(Permissions.AppWorkflows)))
};
public static readonly Roles Empty = new Roles(new ArrayDictionary<string, Role>());
public int CustomCount
{
get { return inner.Count; }
}
public Role this[string name]
{
get { return inner[name]; }
}
public IEnumerable<Role> Custom
{
get { return inner.Values; }
}
public IEnumerable<Role> All
{
get { return inner.Values.Union(Defaults.Values); }
}
private Roles(ArrayDictionary<string, Role> roles)
{
inner = roles;
}
public Roles(IEnumerable<KeyValuePair<string, Role>> items)
{
inner = new ArrayDictionary<string, Role>(Cleaned(items));
}
[Pure]
public Roles Remove(string name)
{
return new Roles(inner.Without(name));
}
[Pure]
public Roles Add(string name)
{
var newRole = new Role(name);
if (inner.ContainsKey(name))
{
throw new ArgumentException("Name already exists.", nameof(name));
}
if (IsDefault(name))
{
return this;
}
return new Roles(inner.With(name, newRole));
}
[Pure]
public Roles Update(string name, params string[] permissions)
{
Guard.NotNullOrEmpty(name);
Guard.NotNull(permissions);
if (!inner.TryGetValue(name, out var role))
{
return this;
}
return new Roles(inner.With(name, role.Update(permissions)));
}
public static bool IsDefault(string role)
{
return role != null && Defaults.ContainsKey(role);
}
public static bool IsDefault(Role role)
{
return role != null && Defaults.ContainsKey(role.Name);
}
public bool ContainsCustom(string name)
{
return inner.ContainsKey(name);
}
public bool Contains(string name)
{
return inner.ContainsKey(name) || Defaults.ContainsKey(name);
}
public bool TryGet(string app, string name, [MaybeNullWhen(false)] out Role value)
{
Guard.NotNull(app, nameof(app));
if (Defaults.TryGetValue(name, out var role) || inner.TryGetValue(name, out role))
{
value = role.ForApp(app);
return true;
}
value = null!;
return false;
}
private static string Clean(string permission)
{
permission = Permissions.ForApp(permission).Id;
var prefix = Permissions.ForApp(Permissions.App);
if (permission.StartsWith(prefix.Id, StringComparison.OrdinalIgnoreCase))
{
permission = permission.Substring(prefix.Id.Length);
}
if (permission.Length == 0)
{
return Permission.Any;
}
return permission.Substring(1);
}
private static KeyValuePair<string, Role>[] Cleaned(IEnumerable<KeyValuePair<string, Role>> items)
{
return items.Where(x => !Defaults.ContainsKey(x.Key)).ToArray();
}
}
}

38
backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Comments
{
public sealed class Comment
{
public Guid Id { get; }
public Instant Time { get; }
public RefToken User { get; }
public string Text { get; }
public Comment(Guid id, Instant time, RefToken user, string text)
{
Guard.NotEmpty(id);
Guard.NotNull(user);
Guard.NotNull(text);
Id = id;
Time = time;
Text = text;
User = user;
}
}
}

99
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentData.cs

@ -0,0 +1,99 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents
{
public abstract class ContentData<T> : Dictionary<T, ContentFieldData?>, IEquatable<ContentData<T>> where T : notnull
{
public IEnumerable<KeyValuePair<T, ContentFieldData?>> ValidValues
{
get { return this.Where(x => x.Value != null); }
}
protected ContentData(IEqualityComparer<T> comparer)
: base(comparer)
{
}
protected ContentData(int capacity, IEqualityComparer<T> comparer)
: base(capacity, comparer)
{
}
protected static TResult MergeTo<TResult>(TResult target, params TResult[] sources) where TResult : ContentData<T>
{
Guard.NotEmpty(sources);
if (sources.Length == 1 || sources.Skip(1).All(x => ReferenceEquals(x, sources[0])))
{
return sources[0];
}
foreach (var source in sources)
{
foreach (var otherValue in source)
{
if (otherValue.Value != null)
{
var fieldValue = target.GetOrAdd(otherValue.Key, x => new ContentFieldData());
if (fieldValue != null)
{
foreach (var value in otherValue.Value)
{
fieldValue[value.Key] = value.Value;
}
}
}
}
}
return target;
}
protected static TResult Clean<TResult>(TResult source, TResult target) where TResult : ContentData<T>
{
foreach (var fieldValue in source.ValidValues)
{
var resultValue = new ContentFieldData();
foreach (var partitionValue in fieldValue.Value.Where(x => x.Value.Type != JsonValueType.Null))
{
resultValue[partitionValue.Key] = partitionValue.Value;
}
if (resultValue.Count > 0)
{
target[fieldValue.Key] = resultValue;
}
}
return target;
}
public override bool Equals(object? obj)
{
return Equals(obj as ContentData<T>);
}
public bool Equals(ContentData<T>? other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other));
}
public override int GetHashCode()
{
return this.DictionaryHashCode();
}
}
}

70
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.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.Collections.Generic;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class ContentFieldData : Dictionary<string, IJsonValue>, IEquatable<ContentFieldData>
{
public ContentFieldData()
: base(StringComparer.OrdinalIgnoreCase)
{
}
public ContentFieldData AddValue(object? value)
{
return AddJsonValue(JsonValue.Create(value));
}
public ContentFieldData AddValue(string key, object? value)
{
return AddJsonValue(key, JsonValue.Create(value));
}
public ContentFieldData AddJsonValue(IJsonValue value)
{
this[InvariantPartitioning.Key] = value;
return this;
}
public ContentFieldData AddJsonValue(string key, IJsonValue value)
{
Guard.NotNullOrEmpty(key);
if (Language.IsValidLanguage(key))
{
this[key] = value;
}
else
{
this[key] = value;
}
return this;
}
public override bool Equals(object? obj)
{
return Equals(obj as ContentFieldData);
}
public bool Equals(ContentFieldData? other)
{
return other != null && (ReferenceEquals(this, other) || this.EqualsDictionary(other));
}
public override int GetHashCode()
{
return this.DictionaryHashCode();
}
}
}

55
backend/src/Squidex.Domain.Apps.Core.Model/Contents/IdContentData.cs

@ -0,0 +1,55 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class IdContentData : ContentData<long>, IEquatable<IdContentData>
{
public IdContentData()
: base(EqualityComparer<long>.Default)
{
}
public IdContentData(int capacity)
: base(capacity, EqualityComparer<long>.Default)
{
}
public static IdContentData Merge(params IdContentData[] contents)
{
return MergeTo(new IdContentData(), contents);
}
public IdContentData MergeInto(IdContentData target)
{
return Merge(target, this);
}
public IdContentData ToCleaned()
{
return Clean(this, new IdContentData());
}
public IdContentData AddField(long id, ContentFieldData? data)
{
Guard.GreaterThan(id, 0);
this[id] = data;
return this;
}
public bool Equals(IdContentData other)
{
return base.Equals(other);
}
}
}

65
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Newtonsoft;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public sealed class ContentFieldDataConverter : JsonClassConverter<ContentFieldData>
{
protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer)
{
writer.WriteStartObject();
foreach (var kvp in value)
{
writer.WritePropertyName(kvp.Key);
serializer.Serialize(writer, kvp.Value);
}
writer.WriteEndObject();
}
protected override ContentFieldData ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer)
{
var result = new ContentFieldData();
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.PropertyName:
var propertyName = reader.Value.ToString()!;
if (!reader.Read())
{
throw new JsonSerializationException("Unexpected end when reading Object.");
}
var value = serializer.Deserialize<IJsonValue>(reader);
if (Language.IsValidLanguage(propertyName) || propertyName == InvariantPartitioning.Key)
{
propertyName = string.Intern(propertyName);
}
result[propertyName] = value;
break;
case JsonToken.EndObject:
return result;
}
}
throw new JsonSerializationException("Unexpected end when reading Object.");
}
}
}

54
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Core.Contents.Json
{
public class JsonWorkflowTransition
{
[JsonProperty]
public string Expression { get; set; }
[JsonProperty]
public string Role { get; set; }
[JsonProperty]
public List<string> Roles { get; }
public JsonWorkflowTransition()
{
}
public JsonWorkflowTransition(WorkflowTransition client)
{
SimpleMapper.Map(client, this);
}
public WorkflowTransition ToTransition()
{
var rolesList = Roles;
if (!string.IsNullOrEmpty(Role))
{
rolesList = new List<string> { Role };
}
ReadOnlyCollection<string>? roles = null;
if (rolesList != null && rolesList.Count > 0)
{
roles = new ReadOnlyCollection<string>(rolesList);
}
return new WorkflowTransition(Expression, roles);
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/StatusConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowConverter.cs

0
src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/WorkflowTransitionConverter.cs

54
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -0,0 +1,54 @@
// ==========================================================================
// 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.Contents
{
public sealed class NamedContentData : ContentData<string>, IEquatable<NamedContentData>
{
public NamedContentData()
: base(StringComparer.Ordinal)
{
}
public NamedContentData(int capacity)
: base(capacity, StringComparer.Ordinal)
{
}
public static NamedContentData Merge(params NamedContentData[] contents)
{
return MergeTo(new NamedContentData(), contents);
}
public NamedContentData MergeInto(NamedContentData target)
{
return Merge(target, this);
}
public NamedContentData ToCleaned()
{
return Clean(this, new NamedContentData());
}
public NamedContentData AddField(string name, ContentFieldData? data)
{
Guard.NotNullOrEmpty(name);
this[name] = data;
return this;
}
public bool Equals(NamedContentData other)
{
return base.Equals(other);
}
}
}

62
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs

@ -0,0 +1,62 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel;
namespace Squidex.Domain.Apps.Core.Contents
{
[TypeConverter(typeof(StatusConverter))]
public struct Status : IEquatable<Status>
{
public static readonly Status Archived = new Status("Archived");
public static readonly Status Draft = new Status("Draft");
public static readonly Status Published = new Status("Published");
private readonly string? name;
public string Name
{
get { return name ?? "Unknown"; }
}
public Status(string? name)
{
this.name = name;
}
public override bool Equals(object? obj)
{
return obj is Status status && Equals(status);
}
public bool Equals(Status other)
{
return string.Equals(name, other.name);
}
public override int GetHashCode()
{
return name?.GetHashCode() ?? 0;
}
public override string ToString()
{
return Name;
}
public static bool operator ==(Status lhs, Status rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(Status lhs, Status rhs)
{
return !lhs.Equals(rhs);
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusChange.cs

0
src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs

36
backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusConverter.cs

@ -0,0 +1,36 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel;
using System.Globalization;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class StatusConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
return destinationType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return new Status(value?.ToString());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
return value.ToString()!;
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs → backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs

126
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -0,0 +1,126 @@
// ==========================================================================
// 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.CodeAnalysis;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflow : Named
{
private const string DefaultName = "Unnamed";
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>();
public static readonly IReadOnlyList<Guid> EmptySchemaIds = new List<Guid>();
public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, EmptySteps);
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps;
public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds;
public Status Initial { get; }
public Workflow(
Status initial,
IReadOnlyDictionary<Status, WorkflowStep>? steps,
IReadOnlyList<Guid>? schemaIds = null,
string? name = null)
: base(name ?? DefaultName)
{
Initial = initial;
if (steps != null)
{
Steps = steps;
}
if (schemaIds != null)
{
SchemaIds = schemaIds;
}
}
public static Workflow CreateDefault(string? name = null)
{
return new Workflow(
Status.Draft, new Dictionary<Status, WorkflowStep>
{
[Status.Archived] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Archived, true),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition()
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
},
StatusColors.Published)
}, null, name);
}
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status)
{
if (TryGetStep(status, out var step))
{
foreach (var transition in step.Transitions)
{
yield return (transition.Key, Steps[transition.Key], transition.Value);
}
}
else if (TryGetStep(Initial, out var initial))
{
yield return (Initial, initial, WorkflowTransition.Default);
}
}
public bool TryGetTransition(Status from, Status to, [MaybeNullWhen(false)] out WorkflowTransition transition)
{
transition = null!;
if (TryGetStep(from, out var step))
{
if (step.Transitions.TryGetValue(to, out transition!))
{
return true;
}
}
else if (to == Initial)
{
transition = WorkflowTransition.Default;
return true;
}
return false;
}
public bool TryGetStep(Status status, [MaybeNullWhen(false)] out WorkflowStep step)
{
return Steps.TryGetValue(status, out step!);
}
public (Status Key, WorkflowStep) GetInitialStep()
{
return (Initial, Steps[Initial]);
}
}
}

31
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class WorkflowStep
{
private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>();
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string? Color { get; }
public bool NoUpdate { get; }
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, bool noUpdate = false)
{
Transitions = transitions ?? EmptyTransitions;
Color = color;
NoUpdate = noUpdate;
}
}
}

27
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class WorkflowTransition
{
public static readonly WorkflowTransition Default = new WorkflowTransition();
public string? Expression { get; }
public ReadOnlyCollection<string>? Roles { get; }
public WorkflowTransition(string? expression = null, ReadOnlyCollection<string>? roles = null)
{
Expression = expression;
Roles = roles;
}
}
}

83
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflows.cs

@ -0,0 +1,83 @@
// ==========================================================================
// 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;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class Workflows : ArrayDictionary<Guid, Workflow>
{
public static readonly Workflows Empty = new Workflows();
private Workflows()
{
}
public Workflows(KeyValuePair<Guid, Workflow>[] items)
: base(items)
{
}
[Pure]
public Workflows Remove(Guid id)
{
return new Workflows(Without(id));
}
[Pure]
public Workflows Add(Guid workflowId, string name)
{
Guard.NotNullOrEmpty(name);
return new Workflows(With(workflowId, Workflow.CreateDefault(name)));
}
[Pure]
public Workflows Set(Workflow workflow)
{
Guard.NotNull(workflow);
return new Workflows(With(Guid.Empty, workflow));
}
[Pure]
public Workflows Set(Guid id, Workflow workflow)
{
Guard.NotNull(workflow);
return new Workflows(With(id, workflow));
}
[Pure]
public Workflows Update(Guid id, Workflow workflow)
{
Guard.NotNull(workflow);
if (id == Guid.Empty)
{
return Set(workflow);
}
if (!ContainsKey(id))
{
return this;
}
return new Workflows(With(id, workflow));
}
public Workflow GetFirst()
{
return Values.FirstOrDefault() ?? Workflow.Default;
}
}
}

0
src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml → backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xml

0
src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd → backend/src/Squidex.Domain.Apps.Core.Model/FodyWeavers.xsd

0
src/Squidex.Domain.Apps.Core.Model/Freezable.cs → backend/src/Squidex.Domain.Apps.Core.Model/Freezable.cs

0
src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs → backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitionItem.cs

0
src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs → backend/src/Squidex.Domain.Apps.Core.Model/IFieldPartitioning.cs

74
backend/src/Squidex.Domain.Apps.Core.Model/InvariantPartitioning.cs

@ -0,0 +1,74 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Squidex.Domain.Apps.Core
{
public sealed class InvariantPartitioning : IFieldPartitioning, IFieldPartitionItem
{
public static readonly InvariantPartitioning Instance = new InvariantPartitioning();
public static readonly string Key = "iv";
public int Count
{
get { return 1; }
}
public IFieldPartitionItem Master
{
get { return this; }
}
string IFieldPartitionItem.Key
{
get { return Key; }
}
string IFieldPartitionItem.Name
{
get { return "Invariant"; }
}
bool IFieldPartitionItem.IsOptional
{
get { return false; }
}
IEnumerable<string> IFieldPartitionItem.Fallback
{
get { return Enumerable.Empty<string>(); }
}
private InvariantPartitioning()
{
}
public bool TryGetItem(string key, [MaybeNullWhen(false)] out IFieldPartitionItem item)
{
var isFound = string.Equals(key, Key, StringComparison.OrdinalIgnoreCase);
item = isFound ? this : null!;
return isFound;
}
IEnumerator<IFieldPartitionItem> IEnumerable<IFieldPartitionItem>.GetEnumerator()
{
yield return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
yield return this;
}
}
}

23
backend/src/Squidex.Domain.Apps.Core.Model/Named.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core
{
public abstract class Named
{
public string Name { get; }
protected Named(string name)
{
Guard.NotNullOrEmpty(name);
Name = name;
}
}
}

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

Loading…
Cancel
Save