mirror of https://github.com/Squidex/squidex.git
Browse Source
* Migration to .NET Core 3.0 * Migration to Orleans 3.0 * Nullable support * Better separation of frontend and backend.pull/443/head
committed by
GitHub
3442 changed files with 94228 additions and 93724 deletions
@ -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 |
|||
@ -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,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,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,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,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,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,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> |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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))); |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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,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,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,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,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>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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)); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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."); |
|||
} |
|||
} |
|||
} |
|||
@ -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,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); |
|||
} |
|||
} |
|||
} |
|||
@ -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,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,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]); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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,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; |
|||
} |
|||
} |
|||
} |
|||
@ -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…
Reference in new issue